verify-company-info-passthrough.mjs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. /**
  2. * 验证适配层把 `result` 事件里的 **company_info 原样带出**(不解析、不渲染进正文)。
  3. *
  4. * 为什么必须测:企业信息同步的第一步就是「拿到 company_info」——
  5. * 原先 `case 'result'` 是个**空分支**,整个 data 被丢弃;本次改成暂存并在 done 时
  6. * 随 `totalResponse` 交给应用侧。这条通路没有别的地方能覆盖(DMS 侧验证是从
  7. * 「已经拿到分类结果」开始的),所以用假 SSE 流喂真实协调器来钉住。
  8. *
  9. * 不联网:把 `globalThis.fetch` 打桩成返回一段构造好的 SSE 流。
  10. *
  11. * 怎么跑(先按 _entry-coordinator.ts 的说明重新打包):
  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-company-info-passthrough.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 === '' ? '' : ` → ${JSON.stringify(extra)}`}`);
  26. }
  27. };
  28. const COMPANY_INFO = {
  29. company: { KeyNo: 'k1', Name: '上海测试科技有限公司', CreditCode: 'TESTCODE1', OperName: '张三' },
  30. profile: { data: { Name: '上海测试科技有限公司', CreditCode: 'TESTCODE1', OperName: '张三' }, source: { provider: '企查查' } },
  31. honors: { records: [], sources: [], complete: true, error: null },
  32. };
  33. const sse = (events) =>
  34. events
  35. .map(([event, data]) => `event: ${event}\ndata: ${JSON.stringify({ thread_id: 't1', request_id: 'r1', data })}\n\n`)
  36. .join('');
  37. /** 把一段 SSE 文本打桩成 fetch 的响应 */
  38. const stubFetchWith = (sseText) => {
  39. globalThis.fetch = async () => {
  40. const stream = new ReadableStream({
  41. start(controller) {
  42. controller.enqueue(new TextEncoder().encode(sseText));
  43. controller.close();
  44. },
  45. });
  46. return new Response(stream, { status: 200, headers: { 'Content-Type': 'text/event-stream' } });
  47. };
  48. };
  49. const runTurn = async (events) => {
  50. stubFetchWith(sse(events));
  51. const coordinator = new ApiChatCoordinator({ baseUrl: 'http://stub', threadId: 't1' });
  52. const seen = { totalResponse: null, messages: [] };
  53. coordinator.addEventListener('totalResponse', (payload) => (seen.totalResponse = payload));
  54. coordinator.addEventListener('message', (msg) => seen.messages.push(msg));
  55. await coordinator.generateAnswer('测试问题');
  56. // flushContent 会等两帧再 emit totalResponse,给它一点时间
  57. await new Promise((resolve) => setTimeout(resolve, 60));
  58. return seen;
  59. };
  60. const BASE_EVENTS = [
  61. ['accepted', { message: '收到' }],
  62. ['answer', { text: '这是回答正文。' }],
  63. ];
  64. console.log('【1】result 带 company_info → totalResponse 原样带出');
  65. {
  66. const seen = await runTurn([
  67. ...BASE_EVENTS,
  68. ['result', { response: '这是回答正文。', company_info: COMPANY_INFO, recommendation: null, errors: [] }],
  69. ['done', { status: 'completed' }],
  70. ]);
  71. check('收到了 totalResponse', !!seen.totalResponse, seen.totalResponse);
  72. check('payload 里有 company_info', !!seen.totalResponse?.company_info);
  73. check(
  74. '内容与后端下发**逐字段一致**(含嵌套的 company/profile/honors)',
  75. JSON.stringify(seen.totalResponse?.company_info) === JSON.stringify(COMPANY_INFO),
  76. seen.totalResponse?.company_info
  77. );
  78. check(
  79. '**没有混进渲染内容**(消息里不应出现 credit code)',
  80. !seen.messages.join('').includes('TESTCODE1'),
  81. seen.messages.map((m) => m.slice(0, 60))
  82. );
  83. }
  84. console.log('\n【2】result 不带 company_info → payload 里也不该有这个键');
  85. {
  86. const seen = await runTurn([
  87. ...BASE_EVENTS,
  88. ['result', { response: '这是回答正文。', recommendation: null, errors: [] }],
  89. ['done', { status: 'completed' }],
  90. ]);
  91. check('没有 company_info 键(而不是 null)', seen.totalResponse && !('company_info' in seen.totalResponse), seen.totalResponse);
  92. }
  93. console.log('\n【3】company_info 为 null / 空对象 → 不往下传(省得白跑一次分类)');
  94. {
  95. const seenNull = await runTurn([
  96. ...BASE_EVENTS,
  97. ['result', { response: 'x', company_info: null }],
  98. ['done', { status: 'completed' }],
  99. ]);
  100. check('company_info=null → 不传', seenNull.totalResponse && !('company_info' in seenNull.totalResponse));
  101. const seenEmpty = await runTurn([
  102. ...BASE_EVENTS,
  103. ['result', { response: 'x', company_info: {} }],
  104. ['done', { status: 'completed' }],
  105. ]);
  106. // 空对象会带出去(适配层不做业务判断),但下游的 isCompanyInfoUsable 会静默跳过
  107. check('company_info={} → 带出去交给下游判断', !!seenEmpty.totalResponse?.company_info, seenEmpty.totalResponse?.company_info);
  108. }
  109. console.log('\n【4】跨轮不串:上一轮有 company_info,下一轮没有 → 不该沿用');
  110. {
  111. stubFetchWith(
  112. sse([
  113. ...BASE_EVENTS,
  114. ['result', { response: 'x', company_info: COMPANY_INFO }],
  115. ['done', { status: 'completed' }],
  116. ])
  117. );
  118. const coordinator = new ApiChatCoordinator({ baseUrl: 'http://stub', threadId: 't1' });
  119. const payloads = [];
  120. coordinator.addEventListener('totalResponse', (payload) => payloads.push(payload));
  121. await coordinator.generateAnswer('第一轮');
  122. await new Promise((resolve) => setTimeout(resolve, 60));
  123. stubFetchWith(
  124. sse([
  125. ...BASE_EVENTS,
  126. ['result', { response: 'x' }], // 这一轮没有 company_info
  127. ['done', { status: 'completed' }],
  128. ])
  129. );
  130. await coordinator.generateAnswer('第二轮');
  131. await new Promise((resolve) => setTimeout(resolve, 60));
  132. check('两轮各收到一次 totalResponse', payloads.length === 2, payloads.length);
  133. check('第一轮带 company_info', !!payloads[0]?.company_info);
  134. check('第二轮**没有**沿用上一轮的 company_info(resetTurnContent 生效)', !('company_info' in (payloads[1] || {})), payloads[1]);
  135. }
  136. console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
  137. process.exit(fail ? 1 : 0);