#!/usr/bin/env node const fs = require('fs'); const os = require('os'); const path = require('path'); const { spawnSync } = require('child_process'); const { buildRealProofClosureWorkOrder } = require('./real-proof-closure-work-order'); const ROOT = path.resolve(__dirname, '..'); const VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS = [ 'mode=unknown', 'collectionMode=unknown', 'generatedBy=unknown', 'preflight.readyForVideoAb=false', 'preflight.failureCount=4', 'preflight.generatedBy=acceptance:video-ab-preflight', 'preflight.proofLevel=not_business_proof', 'preflight.canCloseProofGap=false', 'runtimeCredentialPresent=false', 'companyResolved=false', 'vocSocialProviderTokenPresent=false', 'videoAnalysisTokenPresent=false' ]; const VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS = [ 'proofContext.mode=live', 'proofContext.collectionMode=live', 'proofContext.generatedBy=acceptance:video-ab', 'proofContext.requiresRuntimeCredential=true', 'proofContext.requiresVocSocialProvider=true', 'proofContext.requiresVideoAnalysisProvider=true', 'videoAbPreflight.readyForVideoAb=true', 'videoAbPreflight.proofContext.requiresRuntimeCredential=true', 'videoAbPreflight.company resolved for live run', 'videoAbPreflight.proofContext.requiresVocSocialProvider=true', 'videoAbPreflight.proofContext.requiresVideoAnalysisProvider=true' ]; function main() { const summary = buildRealProofClosureWorkOrder({ root: ROOT, outputsDir: path.join(ROOT, 'outputs') }); assert(summary.passed === true, 'work order should pass against current outputs'); assert(summary.complete === false, 'work order should keep incomplete state while proof gaps remain'); assert(summary.proofLevel === 'not_business_proof', 'work order should be not business proof'); assert(summary.directCustomerProof === false, 'work order must not be direct customer proof'); assert(summary.canCloseProofGap === false, 'work order must not close proof gaps'); assert(summary.requiresHumanRealMaterial === true, 'work order should require human real material'); assert(summary.sourceState.proofGapOpenCount >= 1, 'work order should expose open proof gaps'); assert(summary.sourceState.intakeFailureCount >= 1, 'work order should expose intake failures'); if (summary.sourceState.videoReadyForAbPreflight) { assert(summary.sourceState.realCandidateRows >= 1, 'ready video state should expose real candidate video rows'); } else { assert(summary.sourceState.realCandidateRows === 0, 'blocked video state should expose missing real candidate video'); } assert(summary.workOrderCount >= 5, 'work order should contain actionable rows'); assert(Array.isArray(summary.ownerGroups) && summary.ownerGroups.length >= 1, 'work order should expose current owner groups'); assert(summary.ownerGroups.some(item => item.owner === '商务' && item.workOrderCount >= 1), 'business owner group should exist'); assert(summary.ownerGroups.some(item => item.owner === '技术/AI' && item.workOrderCount >= 1), 'tech/AI owner group should exist'); if (summary.ownerGroups.some(item => item.owner === '商务/投放')) { assert(summary.ownerGroups.some(item => item.owner === '商务/投放' && item.workOrderCount >= 1), 'business/media owner group should exist when current gaps require it'); } assert(summary.workOrders.every(item => item.requiresHumanRealMaterial === true), 'all rows should require real material'); assert(summary.workOrders.every(item => item.directCustomerProof === false), 'all rows should keep proof boundary'); assert(summary.workOrders.every(item => item.canCloseProofGap === false), 'all rows should not close proof gap'); assert(summary.workOrders.every(item => item.targetFile && item.targetFields.length && item.requiredAction && item.acceptanceCommand), 'all rows should be actionable'); assert(summary.workOrders.some(item => item.gapId === 'historical-dataset' && item.targetFile.includes('history-data-template.csv')), 'history gap should target history template'); if (summary.sourceState.videoReadyForAbPreflight) { assert(!summary.workOrders.some(item => item.gapId === 'video-real-candidate'), 'closed video candidate gap should not produce stale work order'); } else { assert(summary.workOrders.some(item => item.gapId === 'video-real-candidate' && item.targetFile.includes('video-resource-template.csv')), 'video candidate gap should target video template'); } 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'); const videoAbWorkOrder = summary.workOrders.find(item => item.gapId === 'video-ab-live-proof'); assert(videoAbWorkOrder, 'work order should include video A/B live proof gap'); assertVideoAbPreflightWorkOrder(videoAbWorkOrder, 'video A/B live proof work order'); assert(summary.workOrders.some(item => item.seedSuggestion && item.seedSuggestion.includes('DHA')), 'work order should carry seed hints without treating them as proof'); assert(summary.guardrails.some(item => item.includes('不写入正式 intake 模板')), 'guardrails should prevent template mutation'); assert(summary.guardrails.some(item => item.includes('不得宣称提号率提升')), 'guardrails should prevent false business claims'); const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-real-proof-work-order-')); const result = spawnSync(process.execPath, [ 'scripts/real-proof-closure-work-order.js', '--outputs', 'outputs', '--output', temp, '--strict' ], { cwd: ROOT, encoding: 'utf8', shell: false }); if (result.stdout) process.stdout.write(result.stdout); if (result.stderr) process.stderr.write(result.stderr); assert(result.status === 0, 'CLI should pass'); const cliSummary = JSON.parse(fs.readFileSync(path.join(temp, 'real-proof-closure-work-order-summary.json'), 'utf8')); const report = fs.readFileSync(path.join(temp, 'real-proof-closure-work-order.md'), 'utf8').replace(/^\uFEFF/, ''); const csv = fs.readFileSync(path.join(temp, 'real-proof-closure-work-order.csv'), 'utf8').replace(/^\uFEFF/, ''); const cliVideoAbWorkOrder = cliSummary.workOrders.find(item => item.gapId === 'video-ab-live-proof'); assert(cliSummary.passed === true, 'CLI summary should pass'); assert(cliSummary.canCloseProofGap === false, 'CLI summary should not close proof gap'); assert(cliVideoAbWorkOrder, 'CLI summary should include video A/B live proof work order'); assertVideoAbPreflightWorkOrder(cliVideoAbWorkOrder, 'CLI video A/B live proof work order'); assert(cliSummary.ownerArtifacts.length === summary.ownerGroups.length, 'CLI should write current owner artifacts'); for (const item of cliSummary.ownerArtifacts) { const slug = ownerSlug(item.owner); assert(fs.existsSync(path.join(temp, 'by-owner', `${slug}.md`)), `${item.owner} owner md should exist`); assert(fs.existsSync(path.join(temp, 'by-owner', `${slug}.csv`)), `${item.owner} owner csv should exist`); } const techOwnerReport = fs.readFileSync(path.join(temp, 'by-owner', 'tech-ai.md'), 'utf8').replace(/^\uFEFF/, ''); assert(report.includes('# 真实证明闭环工单'), 'report should render title'); assert(report.includes('proofGapOpenCount'), 'report should expose open proof gap count'); assert(report.includes('history-data-template.csv'), 'report should include history template'); assert(report.includes('video-resource-template.csv'), 'report should include video template'); assert(report.includes('manual-review-template.csv'), 'report should include review template'); assert(report.includes('不证明客户效果'), 'report should keep proof boundary'); assert(csv.includes('需要人工真实材料'), 'csv should expose real-material flag'); assert(csv.includes('可关闭proof-gap'), 'csv should expose proof-gap boundary'); assertTextIncludesAll(report, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'work order report should preserve video A/B preflight evidence context'); assertTextIncludesAll(report, VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, 'work order report should preserve video A/B preflight missing requirements'); assertTextIncludesAll(csv, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'work order csv should preserve video A/B preflight evidence context'); assertTextIncludesAll(csv, VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, 'work order csv should preserve video A/B preflight missing requirements'); assertTextIncludesAll(techOwnerReport, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'tech/AI work order report should preserve video A/B preflight evidence context'); assertTextIncludesAll(techOwnerReport, VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, 'tech/AI work order report should preserve video A/B preflight missing requirements'); assert(!/sk-[A-Za-z0-9_-]{10,}/.test(report), 'report should not contain model token patterns'); assert(!/r:[A-Za-z0-9]{20,}/.test(report), 'report should not contain Parse sessionToken patterns'); assert(!/Authorization/i.test(report), 'report should not expose Authorization text'); console.log(JSON.stringify({ ok: true, workOrderCount: cliSummary.workOrderCount, ownerArtifactCount: cliSummary.ownerArtifacts.length, proofGapOpenCount: cliSummary.sourceState.proofGapOpenCount }, null, 2)); } function assert(condition, message) { if (!condition) throw new Error(message); } function assertVideoAbPreflightWorkOrder(row, label) { assert(row.source === 'proof-gap-closure', `${label} should come from proof-gap closure`); assert(row.requiresHumanRealMaterial === true, `${label} should require real human material`); assert(row.directCustomerProof === false, `${label} should not be direct customer proof`); assert(row.canCloseProofGap === false, `${label} should not close proof gap`); assert(row.acceptanceCommand === 'npm run acceptance:video-ab', `${label} should keep live video A/B acceptance command`); assertTextIncludesAll(row.currentEvidence || '', VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, `${label} should preserve non-proof preflight evidence context`); assertTextIncludesAll(row.missingProofRequirements || [], VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, `${label} should preserve live proof requirements`); } function assertTextIncludesAll(value, tokens, message) { const text = Array.isArray(value) ? value.join('\n') : String(value || ''); const missing = tokens.filter(token => !text.includes(token)); assert(missing.length === 0, `${message}: missing ${missing.join(', ')}`); } function ownerSlug(owner) { if (owner === '商务') return 'business'; if (owner === '商务/投放') return 'business-media'; if (owner === '技术/AI') return 'tech-ai'; return String(owner || '').replace(/[^\w]+/g, '-').replace(/^-|-$/g, '').toLowerCase(); } if (require.main === module) main();