a15c4177de
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>
236 lines
7.2 KiB
JavaScript
236 lines
7.2 KiB
JavaScript
#!/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);
|
||
});
|