Files
memind/scripts/test-cursor-executor-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

227 lines
7.1 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: Goose 编排 → Cursor executor → MindSpace public/*.html → 会话链接回传
*
* Usage:
* node scripts/test-cursor-executor-e2e.mjs
* node scripts/test-cursor-executor-e2e.mjs --wait-ms=900000
*/
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 {
createReporter,
extractAssistantTexts,
extractPublicLinks,
getSession,
loginViaApi,
resolvePortalBase,
snapshotPublicHtml,
waitForAssistantGrowth,
waitForRunTerminal,
} from './scenario-test-lib.mjs';
loadH5Environment(path.dirname(fileURLToPath(import.meta.url)));
const args = process.argv.slice(2);
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_EXECUTOR_E2E_WAIT_MS
?? 20 * 60 * 1000,
);
const PAGE_BASENAME = `cursor-poem-e2e-${Date.now()}.html`;
const RELATIVE_PATH = `public/${PAGE_BASENAME}`;
const POEM_MESSAGE = [
'帮我写一首关于春日的短诗(四段即可),并做成一个精美的 HTML 页面。',
`产物路径必须是 ${RELATIVE_PATH}`,
].join('\n');
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 createCursorCodeRun(baseUrl, cookie, { message, relativePath }) {
const requestId = crypto.randomUUID();
const body = {
request_id: requestId,
user_message: {
id: crypto.randomUUID(),
role: 'user',
content: [{ type: 'text', text: message }],
metadata: {
userVisible: true,
displayText: message,
memindRun: {
selectedChatSkill: 'aider-development',
validation: {
expectedFile: { path: relativePath },
},
},
},
},
};
const response = await fetch(`${baseUrl}/api/agent/runs`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Cookie: cookie,
},
body: JSON.stringify(body),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(`POST /api/agent/runs ${response.status}: ${JSON.stringify(payload)}`);
}
const run = payload.run ?? payload;
return {
runId: run.id,
requestId,
sessionId: run.sessionId ?? run.agent_session_id ?? null,
status: run.status,
payload,
};
}
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 ?? '888888',
};
console.log('==> Cursor Executor E2EGoose 编排 + Cursor 落盘)');
console.log(` Portal: ${baseUrl}`);
console.log(` 目标文件: ${RELATIVE_PATH}`);
console.log(` 超时: ${waitMs}ms\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 htmlBefore = await snapshotPublicHtml(userId);
const run = await createCursorCodeRun(baseUrl, auth.cookie, {
message: POEM_MESSAGE,
relativePath: RELATIVE_PATH,
});
reporter.pass('提交 code run', run.runId);
const terminal = await waitForRunTerminal(baseUrl, auth.cookie, run.runId, waitMs);
const sessionId = terminal.sessionId ?? terminal.agent_session_id ?? run.sessionId ?? null;
reporter.pass('run 终态', `${terminal.status}${terminal.error ? ` (${terminal.error})` : ''}`);
const pool = createDbPool();
try {
const events = await fetchRunEvents(pool, run.runId);
const dispatch = events.find((item) => item.eventType === 'tool_gateway_dispatch');
const result = events.find((item) => item.eventType === 'tool_gateway_result');
const validation = events.find((item) => item.eventType === 'tool_gateway_validation');
if (dispatch?.data) {
reporter.pass('tool_gateway_dispatch', JSON.stringify(dispatch.data));
} else {
reporter.fail('tool_gateway_dispatch', '未找到 dispatch 事件(可能未走 code executor 路径)');
}
if (result?.data?.executor === 'cursor') {
reporter.pass('executor', 'cursor');
} else {
reporter.fail('executor', `期望 cursor,实际 ${result?.data?.executor ?? 'unknown'}`);
}
if (validation?.data) {
reporter.pass('validation', JSON.stringify(validation.data));
} else if (terminal.status === 'succeeded') {
reporter.fail('validation', '缺少 tool_gateway_validation 事件');
}
} finally {
await pool.end();
}
if (terminal.status !== 'succeeded') {
process.exit(reporter.summary());
}
const htmlAfter = await snapshotPublicHtml(userId);
const created = htmlAfter.find((item) => item.relativePublicPath === RELATIVE_PATH)
?? htmlAfter.filter((item) => !htmlBefore.some((before) => before.fullPath === item.fullPath))
.sort((a, b) => b.mtimeMs - a.mtimeMs)[0]
?? null;
if (created) {
reporter.pass('MindSpace HTML', created.relativePublicPath);
} else {
reporter.fail('MindSpace HTML', `未找到 ${RELATIVE_PATH}`);
}
if (sessionId) {
const reply = await waitForAssistantGrowth(baseUrl, auth.cookie, sessionId, {
minChars: 20,
timeoutMs: 120_000,
});
if (reply?.combined) {
const links = extractPublicLinks(reply.combined, baseUrl);
reporter.pass('assistant 回复', `${reply.combined.length} 字 / ${links.length} 链接`);
if (links.length > 0) {
for (const link of links.slice(0, 3)) {
const pageResponse = await fetch(link, {
headers: cookie ? { Cookie: cookie } : {},
redirect: 'follow',
});
const body = await pageResponse.text();
const hit = ['春', '诗', 'html', 'poem', 'spring'].some((kw) => body.includes(kw));
reporter.pass(
`页面 ${link}`,
`HTTP ${pageResponse.status}${hit ? ' / 含诗歌关键词' : ''}`,
);
}
} else {
reporter.fail('公开链接', '回复中未找到 MindSpace 链接');
}
} else {
reporter.fail('assistant 回复', '会话无新 assistant 消息');
}
} else {
reporter.fail('sessionId', 'run 未回填 sessionId');
}
const exitCode = reporter.summary();
process.exit(exitCode);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});