video-hit-rate-audit-smoke.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. #!/usr/bin/env node
  2. const {
  3. buildAcceptanceGates,
  4. buildRuntimePreflight,
  5. computeVideoAcceptance
  6. } = require('./video-hit-rate-audit');
  7. function main() {
  8. const passing = buildSummary();
  9. passing.acceptance = computeVideoAcceptance(passing);
  10. passing.gates = buildAcceptanceGates(passing);
  11. assert(passing.acceptance.passed, 'complete video A/B evidence should pass');
  12. assert(allGatesPass(passing), 'all gates should pass for complete video A/B evidence');
  13. assert(hasGate(passing, '真实视频 URL 已加载'), 'should gate real video URL loading');
  14. assert(hasGate(passing, 'Top10 证据命中提升且分数不下降'), 'should gate top10 evidence and score non-degradation');
  15. assert(hasGate(passing, '强推荐数量不下降'), 'should gate strong recommendation non-degradation');
  16. assertRuntimePreflight();
  17. assertFails('missing video url should fail', summary => {
  18. summary.enhanced.resourceEvidence.videoUrlCount = 0;
  19. }, 'realVideoResourceLoaded');
  20. assertFails('provider failure should fail', summary => {
  21. summary.enhanced.provider.evidence.providerStatus = 'fallback';
  22. }, 'videoAnalysisProviderOk');
  23. assertFails('pending evidence cards should fail', summary => {
  24. summary.enhanced.evidenceQuality.verifiedCardCount = 0;
  25. }, 'evidenceCardsNotPending');
  26. assertFails('top10 score degradation should fail', summary => {
  27. summary.delta.top10AvgScore = -0.1;
  28. }, 'top10EvidenceImproved');
  29. assertFails('strong count degradation should fail', summary => {
  30. summary.delta.strong = -1;
  31. }, 'strongNotDegraded');
  32. console.log('video hit-rate audit smoke ok');
  33. }
  34. function assertRuntimePreflight() {
  35. const missing = buildRuntimePreflight({
  36. runtimeCredential: '',
  37. company: '',
  38. vocSocialToken: '',
  39. videoToken: '',
  40. videoBaseUrl: 'https://api.fmode.cn',
  41. videoModel: 'doubao-seed-2-0-pro',
  42. brief: 'brief.xlsx',
  43. referenceUrl: 'https://example.com/video',
  44. resultFirstMode: false
  45. }, 'out');
  46. assert(!missing.readyForVideoAb, 'missing runtime inputs should not be ready for video A/B');
  47. assert(missing.failureCount === 4, 'missing runtime inputs should expose four failed checks');
  48. assert(missing.proofLevel === 'not_business_proof', 'runtime preflight should not be business proof');
  49. assert(missing.canCloseProofGap === false, 'runtime preflight cannot close proof gap');
  50. assert(missing.missingProofRequirements.includes('proofContext.requiresRuntimeCredential=true'), 'should expose runtime credential requirement');
  51. assert(missing.missingProofRequirements.includes('proofContext.requiresVideoAnalysisProvider=true'), 'should expose video provider requirement');
  52. assert(missing.proofContext.generatedBy === 'acceptance:video-ab-preflight', 'preflight should not masquerade as acceptance:video-ab');
  53. const ready = buildRuntimePreflight({
  54. runtimeCredential: 'runtime-token',
  55. company: 'company-id',
  56. vocSocialToken: 'voc-social-token',
  57. videoToken: 'video-token',
  58. videoBaseUrl: 'https://api.fmode.cn',
  59. videoModel: 'doubao-seed-2-0-pro',
  60. brief: 'brief.xlsx',
  61. referenceUrl: 'https://example.com/video',
  62. resultFirstMode: true
  63. }, 'out');
  64. assert(ready.readyForVideoAb, 'complete runtime inputs should be ready to run video A/B');
  65. assert(ready.failureCount === 0, 'complete runtime inputs should have no failed checks');
  66. assert(ready.canCloseProofGap === false, 'ready preflight still cannot close proof gap');
  67. assert(ready.nextActions.some(item => item.includes('acceptance:video-ab')), 'ready preflight should point to live A/B run');
  68. }
  69. function buildSummary() {
  70. return {
  71. baseline: {
  72. counts: { candidates: 10, strong: 2, evidenceCards: 0, referenceResources: 0 },
  73. top10: { avgScore: 72, avgReferenceStyleFitScore: 20, evidenceHitCandidates: 0 }
  74. },
  75. enhanced: {
  76. counts: { candidates: 14, strong: 3, evidenceCards: 4, referenceResources: 2 },
  77. provider: { evidence: { providerStatus: 'ok' } },
  78. resourceEvidence: {
  79. referenceResources: 2,
  80. videoUrlCount: 1,
  81. coverUrlCount: 1,
  82. asrTextCount: 0,
  83. frameUrlCount: 0
  84. },
  85. evidenceQuality: {
  86. verifiedCardCount: 3,
  87. cardsWithSignals: 3
  88. },
  89. top10: { avgScore: 73, avgReferenceStyleFitScore: 24, evidenceHitCandidates: 3 }
  90. },
  91. delta: {
  92. candidates: 4,
  93. strong: 1,
  94. evidenceCards: 4,
  95. referenceResources: 2,
  96. top10AvgReferenceStyleFitScore: 4,
  97. top10AvgScore: 1,
  98. top10EvidenceHitCandidates: 3
  99. }
  100. };
  101. }
  102. function assertFails(message, mutate, failedKey) {
  103. const summary = buildSummary();
  104. mutate(summary);
  105. summary.acceptance = computeVideoAcceptance(summary);
  106. summary.gates = buildAcceptanceGates(summary);
  107. assert(!summary.acceptance.passed, message);
  108. assert(summary.acceptance[failedKey] === false, `${message}: ${failedKey} should be false`);
  109. assert(summary.gates.some(gate => gate.status === 'fail'), `${message}: at least one gate should fail`);
  110. }
  111. function allGatesPass(summary) {
  112. return summary.gates.every(gate => gate.status === 'pass');
  113. }
  114. function hasGate(summary, name) {
  115. return summary.gates.some(gate => gate.name === name);
  116. }
  117. function assert(condition, message) {
  118. if (!condition) throw new Error(message);
  119. }
  120. if (require.main === module) main();