test(cursor): add executor routing and help remediation scenarios
Memind CI / Test, build, and release guards (push) Successful in 10m3s
Memind CI / Test, build, and release guards (push) Successful in 10m3s
Add dry-run, e2e, and poem-page remediation scripts for cursor executor and help escalation verification. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* E2E: 验证页面 / 问卷(Page Data) / Excel 分析默认路由到 Cursor executor
|
||||
*
|
||||
* Usage:
|
||||
* JOHN_PASSWORD=... node scripts/test-cursor-task-routing-e2e.mjs
|
||||
* node scripts/test-cursor-task-routing-e2e.mjs --routing-only
|
||||
*/
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { createDbPool } from '../db.mjs';
|
||||
import { loadH5Environment } from './load-env.mjs';
|
||||
import {
|
||||
createAgentRun,
|
||||
createReporter,
|
||||
extractAssistantTexts,
|
||||
extractPublicLinks,
|
||||
loginViaApi,
|
||||
resolvePortalBase,
|
||||
sleep,
|
||||
snapshotPublicHtml,
|
||||
waitForAssistantGrowth,
|
||||
waitForRunTerminal,
|
||||
} from './scenario-test-lib.mjs';
|
||||
|
||||
loadH5Environment(path.dirname(fileURLToPath(import.meta.url)));
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const routingOnly = args.includes('--routing-only');
|
||||
const port = Number(
|
||||
args.find((item) => item.startsWith('--port='))?.slice('--port='.length)
|
||||
?? process.env.H5_PORT
|
||||
?? 8081,
|
||||
);
|
||||
const waitMs = Number(
|
||||
args.find((item) => item.startsWith('--wait-ms='))?.slice('--wait-ms='.length)
|
||||
?? process.env.MEMIND_CURSOR_TASK_ROUTING_E2E_WAIT_MS
|
||||
?? 20 * 60 * 1000,
|
||||
);
|
||||
const routingWaitMs = Number(
|
||||
args.find((item) => item.startsWith('--routing-wait-ms='))?.slice('--routing-wait-ms='.length)
|
||||
?? 5 * 60 * 1000,
|
||||
);
|
||||
|
||||
const CASES = [
|
||||
{
|
||||
id: 'page_generation',
|
||||
label: '页面生成',
|
||||
message: () => `帮我做一个页面 routing-e2e-${Date.now()}`,
|
||||
expectTaskKind: 'page_generation',
|
||||
fullDelivery: true,
|
||||
},
|
||||
{
|
||||
id: 'page_data',
|
||||
label: '调查问卷',
|
||||
message: () => `帮我做一个简单调查问卷(2 道题即可),结果存 PG,页面名 survey-routing-e2e-${Date.now()}`,
|
||||
expectTaskKind: 'page_data',
|
||||
fullDelivery: false,
|
||||
},
|
||||
{
|
||||
id: 'excel_analysis',
|
||||
label: 'Excel 分析',
|
||||
message: () => '帮我分析工作区里 Excel 表格的数据趋势,并做成结果页面',
|
||||
expectTaskKind: 'excel_analysis',
|
||||
fullDelivery: false,
|
||||
},
|
||||
];
|
||||
|
||||
async function fetchRunEvents(pool, runId) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT event_type, data_json, created_at
|
||||
FROM h5_agent_run_events
|
||||
WHERE run_id = ?
|
||||
ORDER BY created_at ASC`,
|
||||
[runId],
|
||||
);
|
||||
return rows.map((row) => ({
|
||||
eventType: row.event_type,
|
||||
data: typeof row.data_json === 'string'
|
||||
? JSON.parse(row.data_json)
|
||||
: (row.data_json ?? null),
|
||||
createdAt: Number(row.created_at),
|
||||
}));
|
||||
}
|
||||
|
||||
async function waitForCursorRouting(pool, runId, timeoutMs) {
|
||||
const started = Date.now();
|
||||
let lastEvents = [];
|
||||
while (Date.now() - started < timeoutMs) {
|
||||
lastEvents = await fetchRunEvents(pool, runId);
|
||||
const result = lastEvents.find((item) => item.eventType === 'tool_gateway_result');
|
||||
if (result?.data?.executor === 'cursor') {
|
||||
return { ok: true, events: lastEvents, result: result.data };
|
||||
}
|
||||
const gooseChat = lastEvents.find((item) => item.eventType === 'direct_chat_completed');
|
||||
if (gooseChat) {
|
||||
return { ok: false, events: lastEvents, reason: '走了 direct_chat(Goose)而非 Cursor' };
|
||||
}
|
||||
await sleep(2000);
|
||||
}
|
||||
const dispatch = lastEvents.find((item) => item.eventType === 'tool_gateway_dispatch');
|
||||
const result = lastEvents.find((item) => item.eventType === 'tool_gateway_result');
|
||||
return {
|
||||
ok: false,
|
||||
events: lastEvents,
|
||||
reason: dispatch
|
||||
? `已 dispatch 但 executor=${result?.data?.executor ?? 'pending'}`
|
||||
: '未看到 tool_gateway_dispatch',
|
||||
};
|
||||
}
|
||||
|
||||
async function verifyCase({
|
||||
baseUrl,
|
||||
cookie,
|
||||
userId,
|
||||
pool,
|
||||
reporter,
|
||||
testCase,
|
||||
}) {
|
||||
console.log(`\n--- ${testCase.label} (${testCase.id}) ---`);
|
||||
const message = testCase.message();
|
||||
console.log(`消息: ${message.slice(0, 80)}${message.length > 80 ? '…' : ''}`);
|
||||
|
||||
const htmlBefore = testCase.fullDelivery ? await snapshotPublicHtml(userId) : [];
|
||||
const run = await createAgentRun(baseUrl, cookie, { message });
|
||||
reporter.pass(`${testCase.label} 提交 run`, run.runId);
|
||||
|
||||
const routing = await waitForCursorRouting(pool, run.runId, routingWaitMs);
|
||||
if (routing.ok) {
|
||||
reporter.pass(`${testCase.label} 路由`, 'tool_gateway → cursor');
|
||||
} else {
|
||||
reporter.fail(`${testCase.label} 路由`, routing.reason ?? 'unknown');
|
||||
if (routing.events?.length) {
|
||||
const types = routing.events.map((item) => item.eventType).join(', ');
|
||||
console.log(` events: ${types}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (routingOnly || !testCase.fullDelivery) {
|
||||
reporter.pass(`${testCase.label} 路由验证`, '仅验证 Cursor 路由(未等待完整交付)');
|
||||
return;
|
||||
}
|
||||
|
||||
const terminal = await waitForRunTerminal(baseUrl, cookie, run.runId, waitMs);
|
||||
reporter.pass(`${testCase.label} run 终态`, `${terminal.status}${terminal.error ? ` (${terminal.error})` : ''}`);
|
||||
|
||||
if (terminal.status !== 'succeeded') {
|
||||
return;
|
||||
}
|
||||
|
||||
const htmlAfter = await snapshotPublicHtml(userId);
|
||||
const created = htmlAfter.filter((item) => !htmlBefore.some((before) => before.fullPath === item.fullPath))
|
||||
.sort((a, b) => b.mtimeMs - a.mtimeMs)[0]
|
||||
?? null;
|
||||
if (created) {
|
||||
reporter.pass(`${testCase.label} MindSpace HTML`, created.relativePublicPath);
|
||||
} else {
|
||||
reporter.fail(`${testCase.label} MindSpace HTML`, '未检测到新 public HTML');
|
||||
}
|
||||
|
||||
const sessionId = terminal.sessionId ?? terminal.agent_session_id ?? run.sessionId ?? null;
|
||||
if (!sessionId) {
|
||||
reporter.fail(`${testCase.label} sessionId`, 'run 未回填 sessionId');
|
||||
return;
|
||||
}
|
||||
|
||||
const reply = await waitForAssistantGrowth(baseUrl, cookie, sessionId, {
|
||||
minChars: 10,
|
||||
timeoutMs: 120_000,
|
||||
});
|
||||
if (reply?.combined) {
|
||||
const links = extractPublicLinks(reply.combined, baseUrl);
|
||||
reporter.pass(`${testCase.label} assistant 回复`, `${reply.combined.length} 字`);
|
||||
if (/cursor/i.test(reply.combined)) {
|
||||
reporter.pass(`${testCase.label} 回复含 cursor`, '已由 cursor 完成执行');
|
||||
}
|
||||
if (links.length > 0) {
|
||||
reporter.pass(`${testCase.label} 公开链接`, links[0]);
|
||||
}
|
||||
} else {
|
||||
reporter.fail(`${testCase.label} assistant 回复`, '无新 assistant 消息');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const reporter = createReporter();
|
||||
const baseUrl = resolvePortalBase(port);
|
||||
const account = {
|
||||
username: process.env.RELEASE_GATE_SCENARIO_USERNAME ?? 'john',
|
||||
password: process.env.JOHN_PASSWORD ?? process.env.H5_ACCESS_PASSWORD ?? '981122tj',
|
||||
};
|
||||
|
||||
console.log('==> Cursor 任务路由 E2E(页面 / 问卷 / Excel)');
|
||||
console.log(` Portal: ${baseUrl}`);
|
||||
console.log(` 模式: ${routingOnly ? '仅路由验证' : '页面全量 + 问卷/Excel 路由'}\n`);
|
||||
|
||||
const statusResponse = await fetch(`${baseUrl}/auth/status`);
|
||||
if (!statusResponse.ok) {
|
||||
throw new Error(`Portal 未就绪: ${statusResponse.status}`);
|
||||
}
|
||||
|
||||
const auth = await loginViaApi(baseUrl, account, reporter);
|
||||
const userId = auth.user?.id;
|
||||
if (!userId) {
|
||||
reporter.fail('登录', '缺少 userId');
|
||||
process.exit(reporter.summary());
|
||||
}
|
||||
|
||||
const pool = createDbPool();
|
||||
try {
|
||||
for (const testCase of CASES) {
|
||||
await verifyCase({
|
||||
baseUrl,
|
||||
cookie: auth.cookie,
|
||||
userId,
|
||||
pool,
|
||||
reporter,
|
||||
testCase,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
process.exit(reporter.summary());
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user