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,226 @@
|
||||
#!/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 E2E(Goose 编排 + 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);
|
||||
});
|
||||
Reference in New Issue
Block a user