real-proof-intake-bundle-smoke.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  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. const VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS = [
  8. 'mode=unknown',
  9. 'collectionMode=unknown',
  10. 'generatedBy=unknown',
  11. 'preflight.readyForVideoAb=false',
  12. 'preflight.failureCount=4',
  13. 'preflight.generatedBy=acceptance:video-ab-preflight',
  14. 'preflight.proofLevel=not_business_proof',
  15. 'preflight.canCloseProofGap=false',
  16. 'runtimeCredentialPresent=false',
  17. 'companyResolved=false',
  18. 'vocSocialProviderTokenPresent=false',
  19. 'videoAnalysisTokenPresent=false'
  20. ];
  21. const VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS = [
  22. 'proofContext.mode=live',
  23. 'proofContext.collectionMode=live',
  24. 'proofContext.generatedBy=acceptance:video-ab',
  25. 'proofContext.requiresRuntimeCredential=true',
  26. 'proofContext.requiresVocSocialProvider=true',
  27. 'proofContext.requiresVideoAnalysisProvider=true',
  28. 'videoAbPreflight.readyForVideoAb=true',
  29. 'videoAbPreflight.proofContext.requiresRuntimeCredential=true',
  30. 'videoAbPreflight.company resolved for live run',
  31. 'videoAbPreflight.proofContext.requiresVocSocialProvider=true',
  32. 'videoAbPreflight.proofContext.requiresVideoAnalysisProvider=true'
  33. ];
  34. function main() {
  35. const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-real-proof-intake-bundle-'));
  36. const result = spawnSync(process.execPath, [
  37. 'scripts/real-proof-intake-bundle.js',
  38. '--output',
  39. outputDir,
  40. '--strict'
  41. ], {
  42. cwd: ROOT,
  43. encoding: 'utf8',
  44. shell: false
  45. });
  46. if (result.stdout) process.stdout.write(result.stdout);
  47. if (result.stderr) process.stderr.write(result.stderr);
  48. assert(result.status === 0, 'real proof intake bundle should pass against current outputs');
  49. const summaryPath = path.join(outputDir, 'real-proof-intake-bundle-summary.json');
  50. const reportPath = path.join(outputDir, 'real-proof-intake-bundle.md');
  51. const csvPath = path.join(outputDir, 'real-proof-intake-bundle.csv');
  52. assert(fs.existsSync(summaryPath), 'summary should exist');
  53. assert(fs.existsSync(reportPath), 'report should exist');
  54. assert(fs.existsSync(csvPath), 'csv should exist');
  55. const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
  56. const report = fs.readFileSync(reportPath, 'utf8').replace(/^\uFEFF/, '');
  57. const csv = fs.readFileSync(csvPath, 'utf8').replace(/^\uFEFF/, '');
  58. const expectedOwnerArtifactCount = Number(summary.expectedOperatorPackOwnerArtifactCount || summary.operatorPackOwnerArtifactCount || 0);
  59. assert(summary.passed === true, 'summary should pass');
  60. assert(summary.complete === false, 'bundle should keep incomplete state while proof gaps remain');
  61. assert(summary.directCustomerProof === false, 'bundle must not be direct customer proof');
  62. assert(summary.proofLevel === 'not_business_proof', 'bundle should classify as not business proof');
  63. assert(summary.proofOpenCount >= 1, 'bundle should expose open proof gap count');
  64. assert(summary.fillRowCount >= 3, 'bundle should expose history/review/video fill rows');
  65. assert(summary.proofGapRowCount === 5, 'bundle should expose five proof gap rows');
  66. assert(summary.recheckCommandCount >= 5, 'bundle should expose recheck commands');
  67. assert(summary.exportRowCount >= summary.fillRowCount + summary.proofGapRowCount, 'csv export should include fill and proof rows');
  68. assert(summary.operatorPackEvidenceRowCount === 5, 'bundle should expose operator pack evidence row count');
  69. assert(summary.operatorPackNextActionCount === 5, 'bundle should expose operator pack next action count');
  70. assert(summary.operatorPackMissingProofRequirementCount >= 1, 'bundle should expose missing proof requirement count');
  71. assert(summary.operatorPackOwnerArtifactCount === expectedOwnerArtifactCount, 'bundle should expose operator pack owner artifact count');
  72. assert(Array.isArray(summary.operatorPackOwnerArtifacts) && summary.operatorPackOwnerArtifacts.length === expectedOwnerArtifactCount, 'bundle should expose operator pack owner artifacts');
  73. assert(summary.operatorPackOwnerArtifacts.some(item => item.owner === '商务' && item.markdown?.includes('by-owner/business.md') && item.csv?.includes('by-owner/business.csv')), 'bundle should expose business owner operator files');
  74. assert(summary.operatorPackOwnerArtifacts.some(item => item.owner === '技术/AI' && item.markdown?.includes('by-owner/tech-ai.md') && item.csv?.includes('by-owner/tech-ai.csv')), 'bundle should expose tech/AI owner operator files');
  75. if (summary.operatorPackOwnerArtifacts.some(item => item.owner === '商务/投放')) {
  76. assert(summary.operatorPackOwnerArtifacts.some(item => item.markdown?.includes('by-owner/business-media.md') && item.csv?.includes('by-owner/business-media.csv')), 'bundle should expose business/media owner operator files when that owner has open actions');
  77. }
  78. assert(summary.operatorPackOwnerArtifacts.every(item => Number(item.evidenceRowCount || 0) >= 1 && Number(item.nextActionCount || 0) >= 1), 'bundle owner artifacts should preserve evidence and next action counts');
  79. assert(summary.proofGapRows.every(item => item.evidence), 'every proof gap row should carry current evidence');
  80. assert(summary.proofGapRows.every(item => item.nextAction), 'every proof gap row should carry next action');
  81. assert(summary.proofGapRows.some(item => Array.isArray(item.missingProofRequirements) && item.missingProofRequirements.length >= 1), 'bundle should carry missing proof requirements from operator pack');
  82. assert(summary.exportRows.filter(item => item.type === '证明缺口').every(item => item.evidence && item.nextAction), 'proof gap export rows should carry evidence and next action');
  83. const videoAbProofGap = summary.proofGapRows.find(item => item.gapId === 'video-ab-live-proof');
  84. assert(videoAbProofGap, 'bundle should include video A/B live proof gap');
  85. assertVideoAbPreflightContext(videoAbProofGap, 'bundle video A/B proof gap row');
  86. const videoAbExportRow = summary.exportRows.find(item => item.type === '证明缺口' && item.evidence?.includes('preflight.readyForVideoAb'));
  87. assert(videoAbExportRow, 'bundle csv export rows should include video A/B preflight evidence row');
  88. assertTextIncludesAll(videoAbExportRow.evidence, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'bundle export row should preserve video A/B preflight evidence context');
  89. assertTextIncludesAll(videoAbExportRow.nextAction, VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, 'bundle export row should preserve video A/B preflight missing requirements');
  90. assert(summary.templateFiles.some(item => item.path?.includes('history-data-template.csv')), 'history template should be included');
  91. assert(summary.templateFiles.some(item => item.path?.includes('manual-review-template.csv')), 'manual review template should be included');
  92. assert(summary.templateFiles.some(item => item.path?.includes('video-resource-template.csv')), 'video template should be included');
  93. assert(summary.templateFiles.some(item => item.path?.includes('intake-field-checklist.md')), 'field checklist should be included');
  94. assert(summary.templateFiles.some(item => item.path?.includes('proof-gap-operator-pack.md')), 'operator pack should be included');
  95. assert(summary.templateFiles.some(item => item.path?.includes('optimization-completion-report.md')), 'optimization completion report should be included');
  96. assert(summary.templateFiles.some(item => item.status?.includes('readyForClaim=false')), 'optimization completion template status should expose readyForClaim=false');
  97. assert(summary.templateFiles.some(item => item.path?.includes('long-run-readiness-report.md')), 'long-run readiness report should be included');
  98. assert(summary.templateFiles.some(item => item.status?.includes('ready=false')), 'long-run readiness template status should expose ready=false');
  99. assert(summary.fillRows.some(item => item.section === 'history' && item.owner === '商务'), 'history fill row should belong to business');
  100. assert(summary.fillRows.some(item => item.section === 'video' && item.owner === '商务/投放'), 'video fill row should belong to business/media');
  101. assert(summary.proofGapRows.some(item => item.gapId === 'manual-review-and-customer-effect'), 'customer effect proof gap should be included');
  102. assert(summary.repairRows.some(item => item.csv?.includes('intake-readiness-repair-actions.csv')), 'data repair csv should be included');
  103. assert(summary.repairRows.some(item => item.csv?.includes('video-resource-repair-actions.csv')), 'video repair csv should be included');
  104. assert(summary.recheckCommands.some(item => item.command.includes('intake:readiness')), 'intake readiness recheck should be included');
  105. assert(summary.recheckCommands.some(item => item.command.includes('video:resource-readiness')), 'video readiness recheck should be included');
  106. assert(summary.recheckCommands.some(item => item.command.includes('customer-effect:audit')), 'customer effect audit should be included');
  107. assert(summary.recheckCommands.some(item => item.command.includes('acceptance:video-ab')), 'video A/B acceptance should be included');
  108. assert(summary.recheckCommands.some(item => item.command.includes('longrun:readiness')), 'long-run readiness recheck should be included');
  109. assert(summary.recheckCommands.some(item => item.command.includes('optimization:completion')), 'optimization completion recheck should be included');
  110. assert(summary.recheckCommands.some(item => item.command.includes('round:refresh')), 'round refresh should be included');
  111. assert(summary.guardrails.some(item => item.includes('proofOpenCount')), 'guardrails should mention proofOpenCount');
  112. assert(summary.guardrails.some(item => item.includes('readyForClaim=false')), 'guardrails should mention readyForClaim=false');
  113. assert(summary.guardrails.some(item => item.includes('longrun:readiness.ready=false')), 'guardrails should mention longrun readiness false');
  114. assert(summary.sourceState.operatorPackEvidenceRowCount === 5, 'source state should expose operator pack evidence row count');
  115. assert(summary.sourceState.operatorPackNextActionCount === 5, 'source state should expose operator pack next action count');
  116. assert(summary.sourceState.operatorPackMissingProofRequirementCount >= 1, 'source state should expose missing proof requirement count');
  117. assert(summary.sourceState.operatorPackOwnerArtifactCount === expectedOwnerArtifactCount, 'source state should expose operator pack owner artifact count');
  118. assert(summary.sourceState.optimizationCompletionComplete === false, 'source state should expose optimization completion incomplete');
  119. assert(summary.sourceState.optimizationCompletionReadyForClaim === false, 'source state should expose optimization completion not ready for claim');
  120. assert(summary.sourceState.longRunReadinessReady === false, 'source state should expose long-run readiness not ready');
  121. assert(summary.sourceState.longRunReadinessFailCount >= 1, 'source state should expose long-run readiness fail count');
  122. assert(summary.sourceState.longRunReadinessProofLevel === 'not_business_proof', 'source state should classify long-run readiness as not business proof');
  123. assert(summary.sourceState.longRunReadinessDirectCustomerProof === false, 'source state should not treat long-run readiness as customer proof');
  124. assert(report.includes('# 真实验收材料收集总包'), 'report should be Chinese');
  125. assert(report.includes('history-data-template.csv'), 'report should include history template path');
  126. assert(report.includes('manual-review-template.csv'), 'report should include manual review template path');
  127. assert(report.includes('video-resource-template.csv'), 'report should include video template path');
  128. assert(report.includes('proof-gap-operator-pack.md'), 'report should include operator pack path');
  129. assert(report.includes('intake-field-checklist.md'), 'report should include field checklist path');
  130. assert(report.includes('optimization-completion-report.md'), 'report should include optimization completion path');
  131. assert(report.includes('readyForClaim=false'), 'report should include optimization completion boundary');
  132. assert(report.includes('long-run-readiness-report.md'), 'report should include long-run readiness path');
  133. assert(report.includes('ready=false'), 'report should include long-run readiness boundary');
  134. assert(report.includes('## 补齐后复验命令'), 'report should include recheck command section');
  135. assert(report.includes('operatorPackEvidenceRowCount:5'), 'report should include operator pack evidence row count');
  136. assert(report.includes('operatorPackNextActionCount:5'), 'report should include operator pack next action count');
  137. assert(report.includes(`operatorPackOwnerArtifactCount:${expectedOwnerArtifactCount}`), 'report should include operator pack owner artifact count');
  138. assert(report.includes('## 操作包负责人附件'), 'report should include operator owner artifact section');
  139. assert(report.includes('by-owner/business.md'), 'report should include business owner operator markdown');
  140. if (summary.operatorPackOwnerArtifacts.some(item => item.owner === '商务/投放')) {
  141. assert(report.includes('by-owner/business-media.csv'), 'report should include business/media owner operator csv when present');
  142. }
  143. assert(report.includes('by-owner/tech-ai.md'), 'report should include tech/AI owner operator markdown');
  144. assert(report.includes('当前证据'), 'report should include current evidence column');
  145. assert(report.includes('下一步'), 'report should include next action column');
  146. assert(report.includes('briefCount=1'), 'report should include concrete history audit evidence');
  147. assert(report.includes('briefCount>=5'), 'report should include concrete history missing proof requirement');
  148. assert(report.includes('acceptance.customerSelectedRateMeasured=true'), 'report should include concrete customer-effect missing proof requirement');
  149. assert(report.includes('补 5-10 个真实 Brief'), 'report should include next action text');
  150. assert(report.includes('npm run longrun:readiness'), 'report should include long-run readiness recheck command');
  151. assert(report.includes('npm run optimization:completion'), 'report should include optimization completion recheck command');
  152. assert(report.includes('不证明客户效果'), 'report should keep proof boundary');
  153. assertTextIncludesAll(report, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'bundle report should preserve video A/B preflight evidence context');
  154. assertTextIncludesAll(report, VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, 'bundle report should preserve video A/B preflight missing requirements');
  155. assert(csv.includes('类型,顺序,负责人,标题,文件或命令'), 'csv should include Chinese header');
  156. assert(csv.includes('当前证据'), 'csv should include current evidence column');
  157. assert(csv.includes('下一步'), 'csv should include next action column');
  158. assert(csv.includes('填表顺序'), 'csv should include fill rows');
  159. assert(csv.includes('证明缺口'), 'csv should include proof gap rows');
  160. assert(csv.includes('负责人附件'), 'csv should include owner artifact rows');
  161. assert(csv.includes('by-owner/business.csv'), 'csv should include business owner operator csv');
  162. if (summary.operatorPackOwnerArtifacts.some(item => item.owner === '商务/投放')) {
  163. assert(csv.includes('by-owner/business-media.md'), 'csv should include business/media owner operator markdown when present');
  164. }
  165. assert(csv.includes('by-owner/tech-ai.csv'), 'csv should include tech/AI owner operator csv');
  166. assertTextIncludesAll(csv, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'bundle csv should preserve video A/B preflight evidence context');
  167. assertTextIncludesAll(csv, VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, 'bundle csv should preserve video A/B preflight missing requirements');
  168. assert(!/sk-[A-Za-z0-9_-]{10,}/.test(report), 'report should not contain model token patterns');
  169. assert(!/r:[A-Za-z0-9]{20,}/.test(report), 'report should not contain Parse sessionToken patterns');
  170. assert(!/Authorization/i.test(report), 'report should not expose Authorization text');
  171. console.log(JSON.stringify({
  172. ok: true,
  173. outputDir,
  174. proofOpenCount: summary.proofOpenCount,
  175. exportRowCount: summary.exportRowCount
  176. }, null, 2));
  177. }
  178. function assert(condition, message) {
  179. if (!condition) throw new Error(message);
  180. }
  181. function assertVideoAbPreflightContext(row, label) {
  182. assert(row.status === 'open', `${label} should remain open until live proof exists`);
  183. assert(row.recheckCommands.some(command => command.includes('acceptance:video-ab')), `${label} should keep live video A/B acceptance command`);
  184. assertTextIncludesAll(row.evidence || '', VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, `${label} should preserve non-proof preflight evidence context`);
  185. assertTextIncludesAll(row.missingProofRequirements || [], VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, `${label} should preserve live proof requirements`);
  186. assertTextIncludesAll(row.nextAction || '', VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, `${label} next action should preserve live proof requirements`);
  187. }
  188. function assertTextIncludesAll(value, tokens, message) {
  189. const text = Array.isArray(value) ? value.join('\n') : String(value || '');
  190. const missing = tokens.filter(token => !text.includes(token));
  191. assert(missing.length === 0, `${message}: missing ${missing.join(', ')}`);
  192. }
  193. if (require.main === module) main();