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>
227 lines
7.1 KiB
JavaScript
227 lines
7.1 KiB
JavaScript
#!/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);
|
||
});
|