Files
memind/scripts/test-cursor-task-routing-e2e.mjs
T
john a15c4177de
Memind CI / Test, build, and release guards (push) Successful in 10m3s
test(cursor): add executor routing and help remediation scenarios
Add dry-run, e2e, and poem-page remediation scripts for cursor executor
and help escalation verification.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 09:33:51 +08:00

236 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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_chatGoose)而非 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);
});