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>
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user