test(cursor): add executor routing and help remediation scenarios
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:
john
2026-08-27 09:33:51 +08:00
parent 8bdb6e2442
commit a15c4177de
8 changed files with 1333 additions and 0 deletions
+235
View File
@@ -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_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);
});