/** * 验证 ApiChatCoordinator **多实例之间的事件隔离**。 * * 为什么必须测:把「同一时间只能一个会话生成」改成「每会话并行」的前提,就是 * 两个协调器实例的事件总线互不串台。一旦串台,A 会话的流式内容会写进 B、 * 停止 A 会把 B 也停掉 —— 也就是重构前的那个 bug 的翻版。 * * 不需要联网:`stopGenerate()` 纯本地;`generateAnswer()` 在空 baseUrl 下 * 直接 emitError('missing_base_url') 短路,不会发请求。 * * 怎么跑(在项目根目录): * npx esbuild harness/tools/_entry-coordinator.ts --bundle --format=esm \ * --outfile=harness/tools/_coordinator.mjs --alias:@=./src --define:import.meta.env='{}' * node harness/tools/verify-coordinator-multi-instance.mjs */ import { ApiChatCoordinator } from './_coordinator.mjs'; let pass = 0; let fail = 0; const check = (name, cond, extra = '') => { if (cond) { pass++; console.log(' ok ' + name); } else { fail++; console.log(' FAIL ' + name + ' ' + extra); } }; /** 给一个协调器实例挂上收集器,返回它收到的事件名数组 */ const instrument = (coordinator) => { const received = []; for (const evt of ['message', 'loadingState', 'totalResponse', 'close', 'error', 'info']) { coordinator.addEventListener(evt, () => received.push(evt)); } return received; }; const makeA = () => new ApiChatCoordinator({ baseUrl: '', threadId: 'thread-A' }); const makeB = () => new ApiChatCoordinator({ baseUrl: '', threadId: 'thread-B' }); console.log('场景:两个会话各自的协调器实例\n'); console.log('【1】停止 A 不得影响 B'); { const a = makeA(); const b = makeB(); const gotA = instrument(a); const gotB = instrument(b); a.stopGenerate(); check('A 收到了自己的 close', gotA.includes('close'), gotA.join(',')); check('A 收到了自己的 loadingState', gotA.includes('loadingState'), gotA.join(',')); check('**B 完全没收到任何事件**', gotB.length === 0, gotB.join(',')); } console.log('\n【2】B 出错/发请求失败不得影响 A'); { const a = makeA(); const b = makeB(); const gotA = instrument(a); const gotB = instrument(b); await b.generateAnswer('问题'); // 空 baseUrl → 立即 emitError,不发网络请求 check('B 收到了 error', gotB.includes('error'), gotB.join(',')); check('B 收到了 info(错误文案通道)', gotB.includes('info'), gotB.join(',')); check('**A 完全没收到任何事件**', gotA.length === 0, gotA.join(',')); } console.log('\n【3】两个实例各自独立停止(并行会话同时生成后各停各的)'); { const a = makeA(); const b = makeB(); const gotA = instrument(a); const gotB = instrument(b); a.stopGenerate(); const afterA = gotB.length; b.stopGenerate(); check('A 停完时 B 仍为零事件', afterA === 0, String(afterA)); check('B 随后收到自己的 close', gotB.includes('close'), gotB.join(',')); check('A 与 B 的事件计数各自独立', gotA.filter((e) => e === 'close').length === 1 && gotB.filter((e) => e === 'close').length === 1); } console.log('\n【4】实例状态互不污染'); { const a = makeA(); const b = makeB(); a.stopGenerate(); check('停 A 后,B 的 threadId 不受影响', b.threadId === 'thread-B', b.threadId); check('两个实例不是同一个对象', a !== b); } console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`); process.exit(fail ? 1 : 0);