verify-dms-payload-fields.mjs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. /**
  2. * 验证写 DMS 时**到底发了哪些字段**(不需要 token,也不碰真实 DMS)。
  3. *
  4. * 为什么要单独测这个:用户明确要求 —— **「我的需求是传 title,不要传类似于 summary
  5. * 的多余的字段」**。字段多发/少发是很容易在后续改动里被"顺手"改掉的,
  6. * 而且它不像业务逻辑那样有直观的行为反馈,所以直接把请求体抓下来断言。
  7. *
  8. * 做法:把 `globalThis.fetch` 打桩,捕获模块真实发出的 form body,
  9. * 解析出 `content` 的键集合,断言「不多不少」。
  10. *
  11. * 怎么跑(在项目根目录):
  12. * npx esbuild harness/tools/_entry-dms.ts --bundle --format=esm \
  13. * --outfile=harness/tools/_dms.mjs --alias:@=./src --define:import.meta.env='{}'
  14. * node harness/tools/verify-dms-payload-fields.mjs
  15. */
  16. globalThis.VITE_DMS_API = 'http://dms.invalid/dms'; // 只为让 base 非空;请求会被打桩拦住
  17. // ── 打桩 fetch:捕获请求,返回可控响应 ────────────────────────────────
  18. const captured = [];
  19. const json = (obj) => new Response(JSON.stringify(obj), { status: 200, headers: { 'Content-Type': 'application/json' } });
  20. globalThis.fetch = async (url, init = {}) => {
  21. const body = init.body ? String(init.body) : '';
  22. captured.push({ url: String(url), body });
  23. if (String(url).includes('/content/selectContentList')) {
  24. return json({ code: 202, message: '数据不存在', content: '数据不存在' }); // 空 → 走新增分支
  25. }
  26. if (String(url).includes('/content/addContent')) {
  27. return json({ code: 200, message: '成功', content: 'stub-uuid-0001' });
  28. }
  29. if (String(url).includes('/content/updateContent')) {
  30. return json({ code: 200, message: '成功', content: '执行成功' });
  31. }
  32. return json({ code: 200, message: '成功', content: '' });
  33. };
  34. const { upsertDmsSession, upsertDmsRecord } = await import('./_dms.mjs');
  35. let pass = 0;
  36. let fail = 0;
  37. const check = (name, cond, extra = '') => {
  38. if (cond) {
  39. pass++;
  40. console.log(' ok ' + name);
  41. } else {
  42. fail++;
  43. console.log(' FAIL ' + name + ' ' + extra);
  44. }
  45. };
  46. /** 取出最近一次某端点的请求体里 content 的字段名(排序后) */
  47. const keysOfLast = (endpoint) => {
  48. const hit = [...captured].reverse().find((c) => c.url.includes(endpoint));
  49. if (!hit) return null;
  50. const params = new URLSearchParams(hit.body);
  51. const content = JSON.parse(params.get('content') || '{}');
  52. return Object.keys(content).sort();
  53. };
  54. const DISALLOWED = ['summary', 'qpyszx', 'qyzt', 'rzqpyx'];
  55. console.log('\n【1】会话新增:字段必须「不多不少」');
  56. await upsertDmsSession({ sessionId: 'sess-A', title: '会话标题甲', creditCode: '91310118TEST' });
  57. let keys = keysOfLast('/content/addContent');
  58. const EXPECT_SESSION_ADD = ['c_created_at', 'c_credit_code', 'c_session_id', 'c_source', 'c_title', 'c_updated_at', 'title'].sort();
  59. check('字段集合与预期完全一致', JSON.stringify(keys) === JSON.stringify(EXPECT_SESSION_ADD), `实际 ${JSON.stringify(keys)}`);
  60. check('**带 title**(用户需求)', keys?.includes('title') === true);
  61. check('不含 summary/qpyszx/qyzt/rzqpyx 等多余字段', !DISALLOWED.some((k) => keys?.includes(k)), JSON.stringify(keys));
  62. console.log('\n【2】会话改名(更新):也要带 title');
  63. captured.length = 0;
  64. // 让这次走「更新」分支:先让 search 返回一行
  65. globalThis.fetch = async (url, init = {}) => {
  66. const body = init.body ? String(init.body) : '';
  67. captured.push({ url: String(url), body });
  68. if (String(url).includes('/content/selectContentList')) {
  69. return json({ code: 200, message: '成功', content: { data: [{ id: 'row-uuid', c_session_id: 'sess-A' }], count: 1 } });
  70. }
  71. return json({ code: 200, message: '成功', content: '执行成功' });
  72. };
  73. await upsertDmsSession({ sessionId: 'sess-A', title: '会话标题乙', creditCode: '91310118TEST' });
  74. keys = keysOfLast('/content/updateContent');
  75. check('更新走的是 updateContent', keys !== null);
  76. check('**更新也带 title**(原先不带,改名后 DMS 里标题不变)', keys?.includes('title') === true, JSON.stringify(keys));
  77. check('更新不带多余的运营字段', !DISALLOWED.some((k) => keys?.includes(k)), JSON.stringify(keys));
  78. console.log('\n【3】问答记录新增:title = 问题文本,且不多发字段');
  79. captured.length = 0;
  80. globalThis.fetch = async (url, init = {}) => {
  81. const body = init.body ? String(init.body) : '';
  82. captured.push({ url: String(url), body });
  83. if (String(url).includes('/content/selectContentList')) return json({ code: 202, message: '数据不存在', content: '数据不存在' });
  84. if (String(url).includes('/content/addContent')) return json({ code: 200, message: '成功', content: 'stub-uuid-0002' });
  85. return json({ code: 200, message: '成功', content: '' });
  86. };
  87. await upsertDmsRecord({ recordId: 'rec-A', sessionId: 'sess-A', creditCode: '91310118TEST', question: '这是问题文本' });
  88. keys = keysOfLast('/content/addContent');
  89. const EXPECT_RECORD_ADD = ['c_created_at', 'c_credit_code', 'c_question', 'c_record_id', 'c_session_id', 'c_source', 'title'].sort();
  90. check('字段集合与预期完全一致', JSON.stringify(keys) === JSON.stringify(EXPECT_RECORD_ADD), `实际 ${JSON.stringify(keys)}`);
  91. check('不含 summary 等多余字段', !DISALLOWED.some((k) => keys?.includes(k)), JSON.stringify(keys));
  92. const hit = [...captured].reverse().find((c) => c.url.includes('/content/addContent'));
  93. const content = JSON.parse(new URLSearchParams(hit.body).get('content'));
  94. check('**title 的值就是问题文本**', content.title === '这是问题文本', JSON.stringify(content.title));
  95. console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
  96. process.exit(fail ? 1 : 0);