Files
memind/scripts/run-scenario-test.mjs
T
john b4171ca268 chore(test): 新增统一测试入口与端到端场景模拟脚本
- scripts/run-memind-tests.mjs:按改动文件自动匹配 test + verify 范围
  (quick/changed/guards/release/full 模式),配套 memind-test skill
- scripts/run-scenario-test.mjs + scenario-test-lib.mjs:真实登录 + 多轮
  聊天 + 页面生成 + 公网可访问性的端到端模拟,配套 memind-scenario-test
  skill 与首个场景 scenarios/suzhou-page.json
- package.json 新增 test:memind / test:scenario 脚本别名

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 10:56:26 +08:00

207 lines
5.6 KiB
JavaScript

#!/usr/bin/env node
/**
* Memind 场景模拟测试入口。
*
* Usage:
* node scripts/run-scenario-test.mjs --scenario suzhou-page
* node scripts/run-scenario-test.mjs --list
* JOHN_PASSWORD=888888 node scripts/run-scenario-test.mjs
*/
import { loadH5Environment } from './load-env.mjs';
import {
createAgentRun,
createReporter,
listScenarios,
loadScenario,
loginViaApi,
resolvePortalBase,
snapshotPublicHtml,
verifyPageAccess,
waitForAssistantGrowth,
waitForRunTerminal,
extractAssistantTexts,
getSession,
} from './scenario-test-lib.mjs';
loadH5Environment(import.meta.dirname);
function usage() {
console.log(`Usage:
node scripts/run-scenario-test.mjs [--scenario <id>] [--list] [--port <port>]
Examples:
node scripts/run-scenario-test.mjs --scenario suzhou-page
node scripts/run-scenario-test.mjs --list`);
}
function parseArgs(argv) {
let scenarioId = 'suzhou-page';
let listOnly = false;
let port = Number(process.env.H5_PORT ?? 8081);
for (let i = 2; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === '--scenario' && argv[i + 1]) {
scenarioId = argv[++i];
} else if (arg === '--port' && argv[i + 1]) {
port = Number(argv[++i]);
} else if (arg === '--list') {
listOnly = true;
} else if (arg === '-h' || arg === '--help') {
usage();
process.exit(0);
} else {
throw new Error(`未知参数: ${arg}`);
}
}
return { scenarioId, listOnly, port };
}
async function ensurePortalReady(baseUrl) {
const response = await fetch(`${baseUrl}/auth/status`);
if (!response.ok) {
throw new Error(`Portal 未就绪: ${baseUrl}/auth/status -> ${response.status}`);
}
}
async function runScenario(scenario, port) {
const reporter = createReporter();
const baseUrl = resolvePortalBase(port);
const account = {
username: scenario.account?.username ?? 'john',
password:
process.env.JOHN_PASSWORD
?? process.env.H5_ACCESS_PASSWORD
?? scenario.account?.password
?? '888888',
};
console.log(`==> 场景: ${scenario.name ?? scenario.id}`);
console.log(` Portal: ${baseUrl}`);
console.log(` 账户: ${account.username}\n`);
await ensurePortalReady(baseUrl);
let auth = null;
let sessionId = null;
let assistantCount = 0;
let assistantCombinedLength = 0;
let publishKey = null;
let htmlBefore = [];
for (const step of scenario.steps ?? []) {
const label = step.label ?? step.action;
console.log(`\n--- ${label} ---`);
if (step.action === 'login') {
auth = await loginViaApi(baseUrl, account, reporter);
publishKey = auth.user?.id ?? auth.user?.publishSlug ?? account.username;
continue;
}
if (step.action === 'chat') {
if (!auth) {
throw new Error('chat 步骤前必须先 login');
}
if (step.expect?.page) {
htmlBefore = await snapshotPublicHtml(publishKey);
}
const run = await createAgentRun(baseUrl, auth.cookie, {
message: step.message,
sessionId,
});
reporter.pass('提交消息', `"${step.message}" → run ${run.runId}`);
const terminal = await waitForRunTerminal(
baseUrl,
auth.cookie,
run.runId,
step.expect?.timeoutMs ?? 300_000,
);
sessionId = terminal.sessionId ?? terminal.agent_session_id ?? run.sessionId ?? sessionId;
if (terminal.status === 'failed') {
reporter.fail('run 终态', terminal.error ?? 'failed');
continue;
}
reporter.pass('run 终态', terminal.status);
if (!sessionId) {
reporter.fail('session', 'run 未回填 sessionId');
continue;
}
const reply = await waitForAssistantGrowth(baseUrl, auth.cookie, sessionId, {
previousCount: assistantCount,
previousCombinedLength: assistantCombinedLength,
minChars: step.expect?.assistantMinChars ?? 1,
timeoutMs: step.expect?.timeoutMs ?? 120_000,
});
if (!reply) {
reporter.fail('assistant 回复', '超时未收到新回复');
continue;
}
assistantCount = reply.count;
assistantCombinedLength = reply.combined.length;
reporter.pass('assistant 回复', `${reply.combined.length} 字 / ${reply.elapsedMs}ms`);
console.log('\n--- 回复摘要 ---');
console.log(reply.combined.slice(0, 600));
const keywords = step.expect?.replyKeywords ?? [];
if (keywords.length) {
const hit = keywords.filter((word) => reply.combined.includes(word));
if (hit.length === 0) {
reporter.fail('回复关键词', `未命中: ${keywords.join(', ')}`);
} else {
reporter.pass('回复关键词', hit.join(', '));
}
}
if (step.expect?.page) {
await verifyPageAccess({
baseUrl,
cookie: auth.cookie,
publishKey,
replyText: reply.combined,
htmlBefore,
expect: step.expect.page,
reporter,
});
}
continue;
}
reporter.fail('未知步骤', step.action ?? 'missing action');
}
return reporter.summary();
}
async function main() {
const { scenarioId, listOnly, port } = parseArgs(process.argv);
if (listOnly) {
const scenarios = await listScenarios();
console.log('可用场景:');
for (const item of scenarios) {
console.log(`- ${item.id}: ${item.name}${item.description ? `${item.description}` : ''}`);
}
return;
}
const scenario = await loadScenario(scenarioId);
const code = await runScenario(scenario, port);
process.exit(code);
}
main().catch((error) => {
console.error(error instanceof Error ? error.stack ?? error.message : error);
process.exit(1);
});