verify-coordinator-multi-instance.mjs 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. /**
  2. * 验证 ApiChatCoordinator **多实例之间的事件隔离**。
  3. *
  4. * 为什么必须测:把「同一时间只能一个会话生成」改成「每会话并行」的前提,就是
  5. * 两个协调器实例的事件总线互不串台。一旦串台,A 会话的流式内容会写进 B、
  6. * 停止 A 会把 B 也停掉 —— 也就是重构前的那个 bug 的翻版。
  7. *
  8. * 不需要联网:`stopGenerate()` 纯本地;`generateAnswer()` 在空 baseUrl 下
  9. * 直接 emitError('missing_base_url') 短路,不会发请求。
  10. *
  11. * 怎么跑(在项目根目录):
  12. * npx esbuild harness/tools/_entry-coordinator.ts --bundle --format=esm \
  13. * --outfile=harness/tools/_coordinator.mjs --alias:@=./src --define:import.meta.env='{}'
  14. * node harness/tools/verify-coordinator-multi-instance.mjs
  15. */
  16. import { ApiChatCoordinator } from './_coordinator.mjs';
  17. let pass = 0;
  18. let fail = 0;
  19. const check = (name, cond, extra = '') => {
  20. if (cond) {
  21. pass++;
  22. console.log(' ok ' + name);
  23. } else {
  24. fail++;
  25. console.log(' FAIL ' + name + ' ' + extra);
  26. }
  27. };
  28. /** 给一个协调器实例挂上收集器,返回它收到的事件名数组 */
  29. const instrument = (coordinator) => {
  30. const received = [];
  31. for (const evt of ['message', 'loadingState', 'totalResponse', 'close', 'error', 'info']) {
  32. coordinator.addEventListener(evt, () => received.push(evt));
  33. }
  34. return received;
  35. };
  36. const makeA = () => new ApiChatCoordinator({ baseUrl: '', threadId: 'thread-A' });
  37. const makeB = () => new ApiChatCoordinator({ baseUrl: '', threadId: 'thread-B' });
  38. console.log('场景:两个会话各自的协调器实例\n');
  39. console.log('【1】停止 A 不得影响 B');
  40. {
  41. const a = makeA();
  42. const b = makeB();
  43. const gotA = instrument(a);
  44. const gotB = instrument(b);
  45. a.stopGenerate();
  46. check('A 收到了自己的 close', gotA.includes('close'), gotA.join(','));
  47. check('A 收到了自己的 loadingState', gotA.includes('loadingState'), gotA.join(','));
  48. check('**B 完全没收到任何事件**', gotB.length === 0, gotB.join(','));
  49. }
  50. console.log('\n【2】B 出错/发请求失败不得影响 A');
  51. {
  52. const a = makeA();
  53. const b = makeB();
  54. const gotA = instrument(a);
  55. const gotB = instrument(b);
  56. await b.generateAnswer('问题'); // 空 baseUrl → 立即 emitError,不发网络请求
  57. check('B 收到了 error', gotB.includes('error'), gotB.join(','));
  58. check('B 收到了 info(错误文案通道)', gotB.includes('info'), gotB.join(','));
  59. check('**A 完全没收到任何事件**', gotA.length === 0, gotA.join(','));
  60. }
  61. console.log('\n【3】两个实例各自独立停止(并行会话同时生成后各停各的)');
  62. {
  63. const a = makeA();
  64. const b = makeB();
  65. const gotA = instrument(a);
  66. const gotB = instrument(b);
  67. a.stopGenerate();
  68. const afterA = gotB.length;
  69. b.stopGenerate();
  70. check('A 停完时 B 仍为零事件', afterA === 0, String(afterA));
  71. check('B 随后收到自己的 close', gotB.includes('close'), gotB.join(','));
  72. check('A 与 B 的事件计数各自独立', gotA.filter((e) => e === 'close').length === 1 && gotB.filter((e) => e === 'close').length === 1);
  73. }
  74. console.log('\n【4】实例状态互不污染');
  75. {
  76. const a = makeA();
  77. const b = makeB();
  78. a.stopGenerate();
  79. check('停 A 后,B 的 threadId 不受影响', b.threadId === 'thread-B', b.threadId);
  80. check('两个实例不是同一个对象', a !== b);
  81. }
  82. console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
  83. process.exit(fail ? 1 : 0);