proof-gap-operator-pack-smoke.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  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-proof-gap-operator-pack-'));
  36. const result = spawnSync(process.execPath, [
  37. 'scripts/proof-gap-operator-pack.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, 'proof gap operator pack should pass against current outputs');
  49. const summaryPath = path.join(outputDir, 'proof-gap-operator-pack-summary.json');
  50. const reportPath = path.join(outputDir, 'proof-gap-operator-pack.md');
  51. const csvPath = path.join(outputDir, 'proof-gap-operator-pack.csv');
  52. const businessOwnerPath = path.join(outputDir, 'by-owner', 'business.md');
  53. const businessOwnerCsvPath = path.join(outputDir, 'by-owner', 'business.csv');
  54. const mediaOwnerPath = path.join(outputDir, 'by-owner', 'business-media.md');
  55. const techOwnerPath = path.join(outputDir, 'by-owner', 'tech-ai.md');
  56. const techOwnerCsvPath = path.join(outputDir, 'by-owner', 'tech-ai.csv');
  57. assert(fs.existsSync(summaryPath), 'summary should exist');
  58. assert(fs.existsSync(reportPath), 'report should exist');
  59. assert(fs.existsSync(csvPath), 'csv should exist');
  60. assert(fs.existsSync(businessOwnerPath), 'business owner markdown should exist');
  61. assert(fs.existsSync(businessOwnerCsvPath), 'business owner csv should exist');
  62. assert(fs.existsSync(techOwnerPath), 'tech/AI owner markdown should exist');
  63. assert(fs.existsSync(techOwnerCsvPath), 'tech/AI owner csv should exist');
  64. const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
  65. const report = fs.readFileSync(reportPath, 'utf8').replace(/^\uFEFF/, '');
  66. const csv = fs.readFileSync(csvPath, 'utf8').replace(/^\uFEFF/, '');
  67. const businessOwnerReport = fs.readFileSync(businessOwnerPath, 'utf8').replace(/^\uFEFF/, '');
  68. const businessOwnerCsv = fs.readFileSync(businessOwnerCsvPath, 'utf8').replace(/^\uFEFF/, '');
  69. const techOwnerReport = fs.readFileSync(techOwnerPath, 'utf8').replace(/^\uFEFF/, '');
  70. const techOwnerCsv = fs.readFileSync(techOwnerCsvPath, 'utf8').replace(/^\uFEFF/, '');
  71. const hasBusinessMediaOwnerArtifact = Array.isArray(summary.ownerArtifacts) &&
  72. summary.ownerArtifacts.some(item => item.markdown === 'by-owner/business-media.md');
  73. if (hasBusinessMediaOwnerArtifact) {
  74. assert(fs.existsSync(mediaOwnerPath), 'business/media owner markdown should exist when generated');
  75. }
  76. assert(summary.passed === true, 'summary should pass');
  77. assert(summary.complete === false, 'summary should keep incomplete state while proof gaps remain');
  78. assert(summary.directCustomerProof === false, 'operator pack must not be direct customer proof');
  79. assert(summary.proofLevel === 'not_business_proof', 'operator pack should classify as not business proof');
  80. assert(summary.rowCount === 5, 'operator pack should expose five business proof gaps');
  81. assert(summary.openCount >= 1, 'operator pack should expose open proof gaps');
  82. assert(Array.isArray(summary.gapIds) && summary.gapIds.includes('historical-dataset') && summary.gapIds.includes('manual-review-and-customer-effect'), 'operator pack should expose stable gap ids');
  83. assert(summary.fillFileCount >= 3, 'operator pack should expose unique fill file count');
  84. assert(summary.recheckCommandCount >= 5, 'operator pack should expose unique recheck command count');
  85. assert(summary.ownerGroupCount >= 2, 'operator pack should expose owner group count');
  86. assert(summary.ownerArtifactCount === summary.ownerGroups.length, 'operator pack should expose one owner artifact per current open owner group');
  87. assert(Array.isArray(summary.ownerArtifacts) && summary.ownerArtifacts.length === summary.ownerGroups.length, 'operator pack should expose owner artifacts');
  88. assert(summary.ownerArtifacts.some(item => item.owner === '商务' && item.markdown === 'by-owner/business.md' && item.csv === 'by-owner/business.csv'), 'operator pack should expose business owner artifact');
  89. if (summary.ownerGroups.some(item => item.owner === '商务/投放')) {
  90. assert(summary.ownerArtifacts.some(item => item.owner === '商务/投放' && item.markdown === 'by-owner/business-media.md' && item.csv === 'by-owner/business-media.csv'), 'operator pack should expose business/media owner artifact when that owner has open actions');
  91. }
  92. assert(summary.ownerArtifacts.some(item => item.owner === '技术/AI' && item.markdown === 'by-owner/tech-ai.md' && item.csv === 'by-owner/tech-ai.csv'), 'operator pack should expose tech/AI owner artifact');
  93. assert(summary.ownerArtifacts.every(item => item.evidenceRowCount >= 1 && item.nextActionCount >= 1), 'every owner artifact should expose evidence and next action counts');
  94. assert(summary.evidenceRowCount === 5, 'operator pack should expose current evidence for every gap row');
  95. assert(summary.nextActionCount === 5, 'operator pack should expose next action for every gap row');
  96. assert(summary.missingProofRequirementCount >= 1, 'operator pack should expose missing proof requirement count');
  97. assert(summary.rows.some(row => row.gapId === 'historical-dataset' && row.owner === '商务'), 'history gap should be assigned to business');
  98. assert(summary.rows.some(row => row.gapId === 'video-real-candidate' && row.owner === '商务/投放'), 'video resource gap should be assigned to business/media');
  99. assert(summary.rows.some(row => row.gapId === 'video-ab-live-proof' && row.owner === '技术/AI'), 'video A/B gap should be assigned to tech/AI');
  100. const videoAbRow = summary.rows.find(row => row.gapId === 'video-ab-live-proof');
  101. assert(videoAbRow, 'video A/B gap row should exist');
  102. assertVideoAbPreflightContext(videoAbRow, 'operator pack video A/B row');
  103. assert(summary.rows.some(row => row.gapId === 'manual-review-and-customer-effect' && row.fillFiles.some(file => file.includes('manual-review-template.csv'))), 'customer effect gap should point to manual review template');
  104. assert(summary.rows.every(row => row.fillFiles.length >= 1), 'every row should expose fill files');
  105. assert(summary.rows.every(row => row.recheckCommands.length >= 1), 'every row should expose recheck commands');
  106. assert(summary.rows.every(row => row.primaryFillFile && row.primaryRecheckCommand), 'every row should expose primary fill/recheck fields');
  107. assert(summary.rows.every(row => row.evidence && row.nextAction), 'every row should expose evidence and next action');
  108. assert(summary.rows.every(row => /不.*证明|不得宣称|不等同于/.test(row.boundary || '')), 'every row should keep proof boundary');
  109. assert(summary.ownerGroups.length >= 2, 'operator pack should expose owner groups');
  110. assert(summary.guardrails.some(item => item.includes('proofOpenCount')), 'guardrails should mention proofOpenCount');
  111. assert(report.includes('# 提号补证操作包'), 'report should be Chinese');
  112. assert(report.includes('缺口 ID'), 'report should include gap id summary');
  113. assert(report.includes('复验命令数'), 'report should include recheck command count');
  114. assert(report.includes('## 负责人文件'), 'report should include owner artifact section');
  115. assert(report.includes('by-owner/business.md'), 'report should include business owner markdown');
  116. if (hasBusinessMediaOwnerArtifact) {
  117. assert(report.includes('by-owner/business-media.csv'), 'report should include business/media owner csv when generated');
  118. }
  119. assert(report.includes('by-owner/tech-ai.md'), 'report should include tech/AI owner markdown');
  120. assert(report.includes('当前证据行数'), 'report should include evidence row count');
  121. assert(report.includes('下一步动作数'), 'report should include next action count');
  122. assert(report.includes('当前证据'), 'report should include current evidence column');
  123. assert(report.includes('下一步'), 'report should include next action column');
  124. assert(report.includes('history-data-template.csv'), 'report should include history template path');
  125. assert(report.includes('video-resource-template.csv'), 'report should include video template path');
  126. assert(report.includes('customer-effect:audit'), 'report should include customer effect audit command');
  127. assert(csv.includes('缺口ID'), 'csv should include Chinese header');
  128. assert(csv.includes('主填写文件'), 'csv should include primary fill file header');
  129. assert(csv.includes('首个复验命令'), 'csv should include primary recheck command header');
  130. assert(csv.includes('当前证据'), 'csv should include current evidence header');
  131. assert(csv.includes('下一步'), 'csv should include next action header');
  132. assert(businessOwnerReport.includes('# 商务提号补证操作包'), 'business owner report should have owner title');
  133. assert(businessOwnerReport.includes('missing historical-dataset-audit.json'), 'business owner report should include current evidence');
  134. assert(businessOwnerReport.includes('补 5-10 个真实 Brief'), 'business owner report should include next action');
  135. assert(businessOwnerCsv.includes('当前证据'), 'business owner csv should include current evidence header');
  136. assert(businessOwnerCsv.includes('下一步'), 'business owner csv should include next action header');
  137. assertTextIncludesAll(report, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'operator pack report should preserve video A/B preflight evidence context');
  138. assertTextIncludesAll(report, VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, 'operator pack report should preserve video A/B preflight missing requirements');
  139. assertTextIncludesAll(csv, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'operator pack csv should preserve video A/B preflight evidence context');
  140. assertTextIncludesAll(techOwnerReport, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'tech/AI owner report should preserve video A/B preflight evidence context');
  141. assertTextIncludesAll(techOwnerReport, VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, 'tech/AI owner report should preserve video A/B preflight missing requirements');
  142. assertTextIncludesAll(techOwnerCsv, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'tech/AI owner csv should preserve video A/B preflight evidence context');
  143. assert(!/sk-[A-Za-z0-9_-]{10,}/.test(report), 'report should not contain model token patterns');
  144. assert(!/r:[A-Za-z0-9]{20,}/.test(report), 'report should not contain Parse sessionToken patterns');
  145. assert(!/Authorization/i.test(report), 'report should not expose Authorization text');
  146. console.log(JSON.stringify({
  147. ok: true,
  148. outputDir,
  149. rowCount: summary.rowCount,
  150. openCount: summary.openCount
  151. }, null, 2));
  152. }
  153. function assert(condition, message) {
  154. if (!condition) throw new Error(message);
  155. }
  156. function assertVideoAbPreflightContext(row, label) {
  157. assert(row.status === 'open', `${label} should remain open until live proof exists`);
  158. assert(row.directCustomerProof !== true, `${label} should not be direct customer proof`);
  159. assert(Array.isArray(row.recheckCommands) && row.recheckCommands.includes('npm run acceptance:video-ab'), `${label} should keep live video A/B acceptance command`);
  160. assertTextIncludesAll(row.evidence || '', VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, `${label} should preserve non-proof preflight evidence context`);
  161. assertTextIncludesAll(row.missingProofRequirements || [], VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, `${label} should preserve live proof requirements`);
  162. }
  163. function assertTextIncludesAll(value, tokens, message) {
  164. const text = Array.isArray(value) ? value.join('\n') : String(value || '');
  165. const missing = tokens.filter(token => !text.includes(token));
  166. assert(missing.length === 0, `${message}: missing ${missing.join(', ')}`);
  167. }
  168. if (require.main === module) main();