intake-field-checklist-smoke.js 5.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const os = require('os');
  4. const path = require('path');
  5. const { spawnSync } = require('child_process');
  6. const root = path.resolve(__dirname, '..');
  7. function main() {
  8. const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-intake-field-checklist-'));
  9. const dataPack = path.join(tmp, 'data');
  10. const videoPack = path.join(tmp, 'video');
  11. const output = path.join(tmp, 'output');
  12. fs.mkdirSync(dataPack, { recursive: true });
  13. fs.mkdirSync(videoPack, { recursive: true });
  14. fs.writeFileSync(path.join(dataPack, 'history-data-template.csv'), [
  15. 'brief编号,项目名称,类目,客户原始Brief,历史人工补号量基线,参考账号或视频,平台,博主名称,主页链接,人工复核标签,客户选择,拒绝原因',
  16. 'brief-001,示例项目,母婴,填写客户原始 Brief,6,https://example.com/reference-video,小红书,示例博主,https://example.com/profile,可直接发客户,待客户反馈,'
  17. ].join('\n'), 'utf8');
  18. fs.writeFileSync(path.join(dataPack, 'manual-review-template.csv'), [
  19. 'brief编号,策略,排名,平台,博主名称,综合分,brief匹配分,参考风格分,主页证据分,视觉质感分,调性一致分,证据加分,证据风险扣分,推荐理由,风险提示,主页链接,人工复核标签,客户选择,归因类型,反馈原因,本轮人工补号量',
  20. 'brief-001,result-first-broad,1,小红书,示例博主,86,82,78,80,79,81,4,0,命中 Brief,需复核,https://example.com/profile,商务复核,待客户反馈,,,3'
  21. ].join('\n'), 'utf8');
  22. fs.writeFileSync(path.join(videoPack, 'video-resource-template.csv'), [
  23. 'brief编号,项目名称,平台,资源角色,博主名称,主页链接,视频链接,封面链接,字幕或ASR文本,帧图链接,标题,发布时间,内容摘要,风格调性标签,画面人设场景信号,风险提示,来源接口或备注,是否真实资源',
  24. 'brief-001,示例项目,小红书,参考视频,示例博主,https://example.com/profile,,https://example.com/cover.jpg,待补ASR,,示例标题,,填写内容摘要,待补调性,,待确认,人工待补,是'
  25. ].join('\n'), 'utf8');
  26. const result = spawnSync(process.execPath, [
  27. path.join(root, 'scripts', 'intake-field-checklist.js'),
  28. '--data-pack',
  29. dataPack,
  30. '--video-pack',
  31. videoPack,
  32. '--output',
  33. output
  34. ], { cwd: root, encoding: 'utf8' });
  35. if (result.stdout) process.stdout.write(result.stdout);
  36. if (result.stderr) process.stderr.write(result.stderr);
  37. assert(result.status === 0, 'field checklist should run');
  38. const summary = JSON.parse(fs.readFileSync(path.join(output, 'intake-field-checklist-summary.json'), 'utf8'));
  39. const report = fs.readFileSync(path.join(output, 'intake-field-checklist.md'), 'utf8').replace(/^\uFEFF/, '');
  40. const csv = fs.readFileSync(path.join(output, 'intake-field-checklist.csv'), 'utf8').replace(/^\uFEFF/, '');
  41. assert(summary.passed === true, 'summary should pass as checklist generated');
  42. assert(summary.directCustomerProof === false, 'summary should preserve proof boundary');
  43. assert(summary.proofLevel === 'smoke_or_local', 'summary should keep local proof level');
  44. assert(summary.itemCount >= 10, 'summary should include field-level checklist items');
  45. assert(summary.missingRequiredCount >= 4, 'summary should include missing required fields and minimum rows');
  46. assert(summary.placeholderCount >= 5, 'summary should include placeholder items');
  47. assert(Array.isArray(summary.ownerGroups) && summary.ownerGroups.length >= 3, 'summary should include owner grouped repair order');
  48. assert(summary.ownerGroups.some(item => item.owner === '商务' && item.section === 'history' && item.nextAction), 'summary should include business history repair group');
  49. assert(summary.ownerGroups.some(item => item.owner === '商务/投放' && item.section === 'video' && item.nextAction), 'summary should include media video repair group');
  50. assert(Array.isArray(summary.recheckCommands) && summary.recheckCommands.some(item => item.command.includes('intake:readiness')), 'summary should include intake readiness recheck command');
  51. assert(summary.recheckCommands.some(item => item.command.includes('round:refresh')), 'summary should include round refresh recheck command');
  52. assert(summary.items.some(item => item.issueType === 'missing_minimum_row' && item.section === 'history'), 'summary should include missing history rows');
  53. assert(summary.items.some(item => item.section === 'video' && item.field === '整行'), 'summary should include missing real candidate video');
  54. assert(report.includes('# 真实材料字段级补齐核对表'), 'report should be Chinese');
  55. assert(report.includes('directCustomerProof:false'), 'report should show proof boundary');
  56. assert(report.includes('## 按负责人补齐顺序'), 'report should include owner grouped repair order');
  57. assert(report.includes('## 补齐后复验命令'), 'report should include recheck commands');
  58. assert(report.includes('npm run intake:readiness'), 'report should include intake readiness command');
  59. assert(report.includes('npm run round:refresh'), 'report should include round refresh command');
  60. assert(csv.includes('负责人,区域,问题类型'), 'csv should use software-friendly Chinese columns');
  61. assert(!/sk-[A-Za-z0-9_-]{10,}/.test(report + csv), 'outputs should not contain model tokens');
  62. assert(!/r:[A-Za-z0-9]{20,}/.test(report + csv), 'outputs should not contain session tokens');
  63. console.log(JSON.stringify({
  64. ok: true,
  65. output,
  66. itemCount: summary.itemCount,
  67. missingRequiredCount: summary.missingRequiredCount,
  68. placeholderCount: summary.placeholderCount
  69. }, null, 2));
  70. }
  71. function assert(condition, message) {
  72. if (!condition) throw new Error(message);
  73. }
  74. main();