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);
|
|||
|
|
});
|