optimization-pipeline-smoke.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  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 { isClaimableProofGapClosure } = require('./optimization-pipeline');
  7. function main() {
  8. const root = path.resolve(__dirname, '..');
  9. assert(isClaimableProofGapClosure({ complete: true, openCount: 0, closedCount: 5, businessGapCount: 5 }), 'complete proof-gap closure with all 5 business gaps should be claimable');
  10. assert(!isClaimableProofGapClosure({ complete: true, openCount: 0, closedCount: 5 }), 'thin proof-gap closure without businessGapCount should not be claimable');
  11. assert(!isClaimableProofGapClosure({ complete: true, openCount: 0, closedCount: 3, businessGapCount: 5 }), 'proof-gap closure with fewer than 5 closed business gaps should not be claimable');
  12. const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-optimization-pipeline-'));
  13. const historyCsv = path.join(dir, 'history.csv');
  14. const reviewCsv = path.join(dir, 'review.csv');
  15. const output = path.join(dir, 'pipeline-output');
  16. const dataPack = path.join(dir, 'data-pack');
  17. const videoPack = path.join(dir, 'video-pack');
  18. writeHistoryCsv(historyCsv);
  19. writeReviewCsv(reviewCsv);
  20. fs.mkdirSync(dataPack, { recursive: true });
  21. fs.mkdirSync(videoPack, { recursive: true });
  22. writeHistoryCsv(path.join(dataPack, 'history-data-template.csv'), { realUrls: true });
  23. writeReviewCsv(path.join(dataPack, 'manual-review-template.csv'), { realUrls: true });
  24. writeVideoCsv(path.join(videoPack, 'video-resource-template.csv'));
  25. const dryRun = spawnSync(process.execPath, [
  26. path.join(root, 'scripts', 'optimization-pipeline.js'),
  27. '--history-csv', historyCsv,
  28. '--review-csv', reviewCsv,
  29. '--history-min-briefs', '3',
  30. '--current-manual-supplement-count', '3',
  31. '--output', path.join(dir, 'dry-run'),
  32. '--dry-run'
  33. ], { cwd: root, encoding: 'utf8' });
  34. assert(dryRun.status === 0, 'dry run should pass');
  35. const drySummary = JSON.parse(fs.readFileSync(path.join(dir, 'dry-run', 'optimization-pipeline-summary.json'), 'utf8'));
  36. assert(drySummary.results.every(item => item.status === 'planned'), 'dry run should only plan steps');
  37. assert(Array.isArray(drySummary.nextActions) && drySummary.nextActions.some(item => item.title.includes('确认输入后执行真实 pipeline')), 'dry run should include next action to run real pipeline');
  38. const dryReport = fs.readFileSync(path.join(dir, 'dry-run', 'optimization-pipeline-report.md'), 'utf8').replace(/^\uFEFF/, '');
  39. assert(dryReport.includes('## 下一步动作'), 'pipeline report should include next actions section');
  40. const packDryRun = spawnSync(process.execPath, [
  41. path.join(root, 'scripts', 'optimization-pipeline.js'),
  42. '--data-pack', dataPack,
  43. '--video-pack', videoPack,
  44. '--history-min-briefs', '3',
  45. '--current-manual-supplement-count', '3',
  46. '--output', path.join(dir, 'pack-dry-run'),
  47. '--dry-run'
  48. ], { cwd: root, encoding: 'utf8' });
  49. assert(packDryRun.status === 0, 'data/video pack dry run should pass');
  50. const packDrySummary = JSON.parse(fs.readFileSync(path.join(dir, 'pack-dry-run', 'optimization-pipeline-summary.json'), 'utf8'));
  51. assert(packDrySummary.results.some(item => item.id === 'intake-readiness' && item.command.includes('intake:readiness')), 'data/video pack pipeline should plan intake readiness');
  52. assert(packDrySummary.results.some(item => item.id === 'video-resource-readiness' && item.command.includes('video:resource-readiness')), 'data/video pack pipeline should plan video resource readiness');
  53. assert(packDrySummary.results.some(item => item.id === 'proof-gap-closure' && item.command.includes('proof-gap:closure')), 'data/video pack pipeline should plan proof gap closure');
  54. assert(packDrySummary.results.some(item => item.command.includes('--intake-readiness')), 'longrun readiness should receive intake readiness summary');
  55. assert(packDrySummary.nextActions.some(item => item.command.includes('optimization:pipeline') && !item.command.includes('--dry-run')), 'pack dry run should tell user to rerun without dry-run');
  56. const run = spawnSync(process.execPath, [
  57. path.join(root, 'scripts', 'optimization-pipeline.js'),
  58. '--history-csv', historyCsv,
  59. '--review-csv', reviewCsv,
  60. '--history-min-briefs', '3',
  61. '--current-manual-supplement-count', '3',
  62. '--output', output
  63. ], { cwd: root, encoding: 'utf8' });
  64. if (run.status !== 0) {
  65. process.stderr.write(run.stderr || run.stdout || '');
  66. throw new Error('optimization pipeline should pass with complete local data');
  67. }
  68. const summary = JSON.parse(fs.readFileSync(path.join(output, 'optimization-pipeline-summary.json'), 'utf8'));
  69. assert(summary.results.some(item => item.id === 'history-from-csv' && item.status === 'pass'), 'pipeline should run history-from-csv');
  70. assert(summary.results.some(item => item.id === 'history-audit' && item.status === 'pass'), 'pipeline should run history-audit');
  71. assert(summary.results.some(item => item.id === 'review-metrics' && item.status === 'pass'), 'pipeline should run review-metrics');
  72. assert(summary.results.some(item => item.id === 'customer-effect-audit' && item.status === 'pass'), 'pipeline should run customer-effect-audit');
  73. assert(summary.results.some(item => item.id === 'proof-gap-closure' && item.status === 'pass'), 'pipeline should run proof-gap-closure');
  74. assert(summary.readyForClaim === false, 'pipeline must not claim business effect while proof gaps remain open');
  75. assert(summary.proofGapClosure && summary.proofGapClosure.complete === false, 'pipeline should expose incomplete proof gap closure');
  76. assert(summary.proofGapClosure && summary.proofGapClosure.claimable === false, 'pipeline should expose non-claimable proof-gap closure state');
  77. assert(summary.nextActions.some(item => item.step === 'proof-gap-closure'), 'pipeline should ask to close proof gaps when closure is incomplete');
  78. assert(Array.isArray(summary.nextActions), 'pipeline should always output nextActions array');
  79. assert(fs.existsSync(path.join(output, 'customer-effect-audit', 'customer-effect-summary.json')), 'pipeline should write customer effect summary');
  80. assert(fs.existsSync(path.join(output, 'proof-gap-closure', 'proof-gap-closure-summary.json')), 'pipeline should write proof gap closure summary');
  81. assert(fs.existsSync(path.join(output, 'evidence-index', 'evidence-index-summary.json')), 'pipeline should write evidence index');
  82. console.log(JSON.stringify({ ok: true, output, steps: summary.results.length }, null, 2));
  83. }
  84. function writeHistoryCsv(file, options = {}) {
  85. const url = options.realUrls ? 'https://www.xiaohongshu.com/user/profile' : 'https://example.com';
  86. fs.writeFileSync(file, withBom([
  87. 'brief编号,项目名称,类目,客户原始Brief,历史人工补号量基线,参考账号或视频,平台,博主名称,主页链接,人工复核标签,客户选择,拒绝原因',
  88. `brief-a,历史A,护肤,敏感肌修护真实历史Brief,6,https://www.xiaohongshu.com/explore/ref-a,小红书,敏感肌成分党,${url}/a1,可直接发客户,客户选中,`,
  89. `brief-a,历史A,护肤,敏感肌修护真实历史Brief,6,https://www.xiaohongshu.com/explore/ref-a,小红书,硬广导购号,${url}/a2,跑偏,客户拒绝,硬广感太强`,
  90. `brief-b,历史B,母婴,母婴DHA真实历史Brief,6,https://www.xiaohongshu.com/explore/ref-b,小红书,营养师妈妈,${url}/b1,商务复核,客户选中,`,
  91. `brief-b,历史B,母婴,母婴DHA真实历史Brief,6,https://www.xiaohongshu.com/explore/ref-b,小红书,泛生活号,${url}/b2,跑偏,客户拒绝,人设太泛`,
  92. `brief-c,历史C,家清,家清真实历史Brief,6,https://www.douyin.com/user/ref-c,抖音,家清实验室,${url}/c1,可直接发客户,客户选中,`,
  93. `brief-c,历史C,家清,家清真实历史Brief,6,https://www.douyin.com/user/ref-c,抖音,装修设计号,${url}/c2,跑偏,客户拒绝,内容偏装修`
  94. ].join('\n')), 'utf8');
  95. }
  96. function writeReviewCsv(file, options = {}) {
  97. const url = options.realUrls ? 'https://www.xiaohongshu.com/user/profile' : 'http://example.com';
  98. fs.writeFileSync(file, withBom([
  99. 'brief编号,策略,排名,平台,博主名称,综合分,brief匹配分,参考风格分,主页证据分,视觉质感分,调性一致分,证据加分,证据风险扣分,推荐理由,风险提示,主页链接,人工复核标签,客户选择,归因类型,本轮人工补号量',
  100. `brief-a,baseline-live,1,小红书,基础A,80,80,40,60,70,70,0,0,基础召回,需复核,${url}/r1,商务复核,客户选中,,3`,
  101. `brief-a,baseline-live,2,小红书,基础B,70,70,40,60,70,70,0,0,基础召回,跑偏,${url}/r2,跑偏,,召回关键词错,3`,
  102. `brief-a,baseline-live,3,小红书,基础C,80,80,40,60,70,70,0,0,基础召回,需复核,${url}/r3,商务复核,客户选中,,3`,
  103. `brief-a,reference-account,4,小红书,参考A,90,90,88,85,85,88,2,0,参考相似,需复核,${url}/r4,可直接发客户,客户选中,,3`,
  104. `brief-a,reference-account,5,小红书,参考B,89,89,87,85,85,88,2,0,参考相似,需复核,${url}/r5,商务复核,客户选中,,3`,
  105. `brief-a,homepage-evidence,6,小红书,主页A,88,88,82,90,85,88,2,0,主页匹配,需复核,${url}/r6,商务复核,客户选中,,3`,
  106. `brief-a,homepage-evidence,7,小红书,主页B,87,87,82,89,85,88,2,0,主页匹配,需复核,${url}/r7,商务复核,客户选中,,3`,
  107. `brief-a,video-enhanced,8,小红书,视频A,86,86,84,88,85,88,2,0,视频匹配,需复核,${url}/r8,商务复核,客户选中,,3`,
  108. `brief-a,video-enhanced,9,小红书,视频B,85,85,84,88,85,88,2,0,视频匹配,需复核,${url}/r9,商务复核,,,3`,
  109. `brief-a,result-first,10,小红书,结果A,84,84,80,86,84,86,2,0,综合匹配,需复核,${url}/r10,商务复核,客户选中,,3`
  110. ].join('\n')), 'utf8');
  111. }
  112. function writeVideoCsv(file) {
  113. fs.writeFileSync(file, withBom([
  114. 'brief编号,项目名称,平台,资源角色,博主名称,主页链接,视频链接,封面链接,字幕或ASR文本,帧图链接,标题,发布时间,内容摘要,风格调性标签,画面人设场景信号,风险提示,来源接口或备注,是否真实资源',
  115. 'brief-a,历史A,小红书,参考视频,参考账号,https://www.xiaohongshu.com/user/profile/ref-a,https://www.xiaohongshu.com/explore/video-a,https://sns-img.example.cn/a.jpg,真实敏感肌修护参考视频字幕文本,https://sns-img.example.cn/a-frame.jpg,参考视频标题,2026-06-01,真实参考内容摘要,真实记录/成分党,近景护肤场景,无,人工补充,是',
  116. 'brief-a,历史A,小红书,候选视频,候选账号,https://www.xiaohongshu.com/user/profile/candidate-a,https://www.xiaohongshu.com/explore/video-b,https://sns-img.example.cn/b.jpg,真实候选视频字幕文本用于风格分析,https://sns-img.example.cn/b-frame.jpg,候选视频标题,2026-06-02,真实候选内容摘要,真实记录/测评,居家护肤场景,无,人工补充,是'
  117. ].join('\n')), 'utf8');
  118. }
  119. function withBom(text) {
  120. return `\uFEFF${text}`;
  121. }
  122. function assert(condition, message) {
  123. if (!condition) throw new Error(message);
  124. }
  125. if (require.main === module) main();