real-proof-closure-work-order-smoke.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  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 { buildRealProofClosureWorkOrder } = require('./real-proof-closure-work-order');
  7. const ROOT = path.resolve(__dirname, '..');
  8. const VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS = [
  9. 'mode=unknown',
  10. 'collectionMode=unknown',
  11. 'generatedBy=unknown',
  12. 'preflight.readyForVideoAb=false',
  13. 'preflight.failureCount=4',
  14. 'preflight.generatedBy=acceptance:video-ab-preflight',
  15. 'preflight.proofLevel=not_business_proof',
  16. 'preflight.canCloseProofGap=false',
  17. 'runtimeCredentialPresent=false',
  18. 'companyResolved=false',
  19. 'vocSocialProviderTokenPresent=false',
  20. 'videoAnalysisTokenPresent=false'
  21. ];
  22. const VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS = [
  23. 'proofContext.mode=live',
  24. 'proofContext.collectionMode=live',
  25. 'proofContext.generatedBy=acceptance:video-ab',
  26. 'proofContext.requiresRuntimeCredential=true',
  27. 'proofContext.requiresVocSocialProvider=true',
  28. 'proofContext.requiresVideoAnalysisProvider=true',
  29. 'videoAbPreflight.readyForVideoAb=true',
  30. 'videoAbPreflight.proofContext.requiresRuntimeCredential=true',
  31. 'videoAbPreflight.company resolved for live run',
  32. 'videoAbPreflight.proofContext.requiresVocSocialProvider=true',
  33. 'videoAbPreflight.proofContext.requiresVideoAnalysisProvider=true'
  34. ];
  35. function main() {
  36. const summary = buildRealProofClosureWorkOrder({
  37. root: ROOT,
  38. outputsDir: path.join(ROOT, 'outputs')
  39. });
  40. assert(summary.passed === true, 'work order should pass against current outputs');
  41. assert(summary.complete === false, 'work order should keep incomplete state while proof gaps remain');
  42. assert(summary.proofLevel === 'not_business_proof', 'work order should be not business proof');
  43. assert(summary.directCustomerProof === false, 'work order must not be direct customer proof');
  44. assert(summary.canCloseProofGap === false, 'work order must not close proof gaps');
  45. assert(summary.requiresHumanRealMaterial === true, 'work order should require human real material');
  46. assert(summary.sourceState.proofGapOpenCount >= 1, 'work order should expose open proof gaps');
  47. assert(summary.sourceState.intakeFailureCount >= 1, 'work order should expose intake failures');
  48. if (summary.sourceState.videoReadyForAbPreflight) {
  49. assert(summary.sourceState.realCandidateRows >= 1, 'ready video state should expose real candidate video rows');
  50. } else {
  51. assert(summary.sourceState.realCandidateRows === 0, 'blocked video state should expose missing real candidate video');
  52. }
  53. assert(summary.workOrderCount >= 5, 'work order should contain actionable rows');
  54. assert(Array.isArray(summary.ownerGroups) && summary.ownerGroups.length >= 1, 'work order should expose current owner groups');
  55. assert(summary.ownerGroups.some(item => item.owner === '商务' && item.workOrderCount >= 1), 'business owner group should exist');
  56. assert(summary.ownerGroups.some(item => item.owner === '技术/AI' && item.workOrderCount >= 1), 'tech/AI owner group should exist');
  57. if (summary.ownerGroups.some(item => item.owner === '商务/投放')) {
  58. assert(summary.ownerGroups.some(item => item.owner === '商务/投放' && item.workOrderCount >= 1), 'business/media owner group should exist when current gaps require it');
  59. }
  60. assert(summary.workOrders.every(item => item.requiresHumanRealMaterial === true), 'all rows should require real material');
  61. assert(summary.workOrders.every(item => item.directCustomerProof === false), 'all rows should keep proof boundary');
  62. assert(summary.workOrders.every(item => item.canCloseProofGap === false), 'all rows should not close proof gap');
  63. assert(summary.workOrders.every(item => item.targetFile && item.targetFields.length && item.requiredAction && item.acceptanceCommand), 'all rows should be actionable');
  64. assert(summary.workOrders.some(item => item.gapId === 'historical-dataset' && item.targetFile.includes('history-data-template.csv')), 'history gap should target history template');
  65. if (summary.sourceState.videoReadyForAbPreflight) {
  66. assert(!summary.workOrders.some(item => item.gapId === 'video-real-candidate'), 'closed video candidate gap should not produce stale work order');
  67. } else {
  68. assert(summary.workOrders.some(item => item.gapId === 'video-real-candidate' && item.targetFile.includes('video-resource-template.csv')), 'video candidate gap should target video template');
  69. }
  70. assert(summary.workOrders.some(item => item.gapId === 'manual-review-and-customer-effect' && item.targetFile.includes('manual-review-template.csv')), 'customer effect gap should target review template');
  71. const videoAbWorkOrder = summary.workOrders.find(item => item.gapId === 'video-ab-live-proof');
  72. assert(videoAbWorkOrder, 'work order should include video A/B live proof gap');
  73. assertVideoAbPreflightWorkOrder(videoAbWorkOrder, 'video A/B live proof work order');
  74. assert(summary.workOrders.some(item => item.seedSuggestion && item.seedSuggestion.includes('DHA')), 'work order should carry seed hints without treating them as proof');
  75. assert(summary.guardrails.some(item => item.includes('不写入正式 intake 模板')), 'guardrails should prevent template mutation');
  76. assert(summary.guardrails.some(item => item.includes('不得宣称提号率提升')), 'guardrails should prevent false business claims');
  77. const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-real-proof-work-order-'));
  78. const result = spawnSync(process.execPath, [
  79. 'scripts/real-proof-closure-work-order.js',
  80. '--outputs',
  81. 'outputs',
  82. '--output',
  83. temp,
  84. '--strict'
  85. ], {
  86. cwd: ROOT,
  87. encoding: 'utf8',
  88. shell: false
  89. });
  90. if (result.stdout) process.stdout.write(result.stdout);
  91. if (result.stderr) process.stderr.write(result.stderr);
  92. assert(result.status === 0, 'CLI should pass');
  93. const cliSummary = JSON.parse(fs.readFileSync(path.join(temp, 'real-proof-closure-work-order-summary.json'), 'utf8'));
  94. const report = fs.readFileSync(path.join(temp, 'real-proof-closure-work-order.md'), 'utf8').replace(/^\uFEFF/, '');
  95. const csv = fs.readFileSync(path.join(temp, 'real-proof-closure-work-order.csv'), 'utf8').replace(/^\uFEFF/, '');
  96. const cliVideoAbWorkOrder = cliSummary.workOrders.find(item => item.gapId === 'video-ab-live-proof');
  97. assert(cliSummary.passed === true, 'CLI summary should pass');
  98. assert(cliSummary.canCloseProofGap === false, 'CLI summary should not close proof gap');
  99. assert(cliVideoAbWorkOrder, 'CLI summary should include video A/B live proof work order');
  100. assertVideoAbPreflightWorkOrder(cliVideoAbWorkOrder, 'CLI video A/B live proof work order');
  101. assert(cliSummary.ownerArtifacts.length === summary.ownerGroups.length, 'CLI should write current owner artifacts');
  102. for (const item of cliSummary.ownerArtifacts) {
  103. const slug = ownerSlug(item.owner);
  104. assert(fs.existsSync(path.join(temp, 'by-owner', `${slug}.md`)), `${item.owner} owner md should exist`);
  105. assert(fs.existsSync(path.join(temp, 'by-owner', `${slug}.csv`)), `${item.owner} owner csv should exist`);
  106. }
  107. const techOwnerReport = fs.readFileSync(path.join(temp, 'by-owner', 'tech-ai.md'), 'utf8').replace(/^\uFEFF/, '');
  108. assert(report.includes('# 真实证明闭环工单'), 'report should render title');
  109. assert(report.includes('proofGapOpenCount'), 'report should expose open proof gap count');
  110. assert(report.includes('history-data-template.csv'), 'report should include history template');
  111. assert(report.includes('video-resource-template.csv'), 'report should include video template');
  112. assert(report.includes('manual-review-template.csv'), 'report should include review template');
  113. assert(report.includes('不证明客户效果'), 'report should keep proof boundary');
  114. assert(csv.includes('需要人工真实材料'), 'csv should expose real-material flag');
  115. assert(csv.includes('可关闭proof-gap'), 'csv should expose proof-gap boundary');
  116. assertTextIncludesAll(report, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'work order report should preserve video A/B preflight evidence context');
  117. assertTextIncludesAll(report, VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, 'work order report should preserve video A/B preflight missing requirements');
  118. assertTextIncludesAll(csv, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'work order csv should preserve video A/B preflight evidence context');
  119. assertTextIncludesAll(csv, VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, 'work order csv should preserve video A/B preflight missing requirements');
  120. assertTextIncludesAll(techOwnerReport, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'tech/AI work order report should preserve video A/B preflight evidence context');
  121. assertTextIncludesAll(techOwnerReport, VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, 'tech/AI work order report should preserve video A/B preflight missing requirements');
  122. assert(!/sk-[A-Za-z0-9_-]{10,}/.test(report), 'report should not contain model token patterns');
  123. assert(!/r:[A-Za-z0-9]{20,}/.test(report), 'report should not contain Parse sessionToken patterns');
  124. assert(!/Authorization/i.test(report), 'report should not expose Authorization text');
  125. console.log(JSON.stringify({
  126. ok: true,
  127. workOrderCount: cliSummary.workOrderCount,
  128. ownerArtifactCount: cliSummary.ownerArtifacts.length,
  129. proofGapOpenCount: cliSummary.sourceState.proofGapOpenCount
  130. }, null, 2));
  131. }
  132. function assert(condition, message) {
  133. if (!condition) throw new Error(message);
  134. }
  135. function assertVideoAbPreflightWorkOrder(row, label) {
  136. assert(row.source === 'proof-gap-closure', `${label} should come from proof-gap closure`);
  137. assert(row.requiresHumanRealMaterial === true, `${label} should require real human material`);
  138. assert(row.directCustomerProof === false, `${label} should not be direct customer proof`);
  139. assert(row.canCloseProofGap === false, `${label} should not close proof gap`);
  140. assert(row.acceptanceCommand === 'npm run acceptance:video-ab', `${label} should keep live video A/B acceptance command`);
  141. assertTextIncludesAll(row.currentEvidence || '', VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, `${label} should preserve non-proof preflight evidence context`);
  142. assertTextIncludesAll(row.missingProofRequirements || [], VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, `${label} should preserve live proof requirements`);
  143. }
  144. function assertTextIncludesAll(value, tokens, message) {
  145. const text = Array.isArray(value) ? value.join('\n') : String(value || '');
  146. const missing = tokens.filter(token => !text.includes(token));
  147. assert(missing.length === 0, `${message}: missing ${missing.join(', ')}`);
  148. }
  149. function ownerSlug(owner) {
  150. if (owner === '商务') return 'business';
  151. if (owner === '商务/投放') return 'business-media';
  152. if (owner === '技术/AI') return 'tech-ai';
  153. return String(owner || '').replace(/[^\w]+/g, '-').replace(/^-|-$/g, '').toLowerCase();
  154. }
  155. if (require.main === module) main();