/ tests / pipeline / compute-score.test.js
compute-score.test.js
  1  import { test, describe } from 'node:test';
  2  import assert from 'node:assert/strict';
  3  import { computeScoreFromFactors, computeGrade } from '../../src/score.js';
  4  
  5  describe('computeScoreFromFactors', () => {
  6    test('computes weighted score from factor scores', () => {
  7      const factors = {
  8        headline_quality: { score: 8, reasoning: 'Good' },
  9        value_proposition: { score: 7, reasoning: 'OK' },
 10        unique_selling_proposition: { score: 6, reasoning: 'Weak' },
 11        call_to_action: { score: 9, reasoning: 'Great' },
 12        urgency_messaging: { score: 3, reasoning: 'Missing' },
 13        hook_engagement: { score: 7, reasoning: 'Good' },
 14        trust_signals: { score: 5, reasoning: 'Basic' },
 15        imagery_design: { score: 8, reasoning: 'Nice' },
 16        offer_clarity: { score: 8, reasoning: 'Clear' },
 17        contextual_appropriateness: { score: 7, reasoning: 'Fits' },
 18      };
 19  
 20      const score = computeScoreFromFactors(factors);
 21      // Manual: (8*.15)+(7*.14)+(6*.13)+(9*.13)+(3*.10)+(7*.09)+(5*.11)+(8*.08)+(8*.04)+(7*.03)
 22      // = 1.2 + 0.98 + 0.78 + 1.17 + 0.30 + 0.63 + 0.55 + 0.64 + 0.32 + 0.21 = 6.78
 23      // 6.78 * 10 = 67.8
 24      assert.equal(score, 67.8);
 25    });
 26  
 27    test('returns 0 for all-zero scores', () => {
 28      const factors = {
 29        headline_quality: { score: 0 },
 30        value_proposition: { score: 0 },
 31        unique_selling_proposition: { score: 0 },
 32        call_to_action: { score: 0 },
 33        urgency_messaging: { score: 0 },
 34        hook_engagement: { score: 0 },
 35        trust_signals: { score: 0 },
 36        imagery_design: { score: 0 },
 37        offer_clarity: { score: 0 },
 38        contextual_appropriateness: { score: 0 },
 39      };
 40      assert.equal(computeScoreFromFactors(factors), 0);
 41    });
 42  
 43    test('returns 100 for all-perfect scores', () => {
 44      const factors = {
 45        headline_quality: { score: 10 },
 46        value_proposition: { score: 10 },
 47        unique_selling_proposition: { score: 10 },
 48        call_to_action: { score: 10 },
 49        urgency_messaging: { score: 10 },
 50        hook_engagement: { score: 10 },
 51        trust_signals: { score: 10 },
 52        imagery_design: { score: 10 },
 53        offer_clarity: { score: 10 },
 54        contextual_appropriateness: { score: 10 },
 55      };
 56      assert.equal(computeScoreFromFactors(factors), 100);
 57    });
 58  
 59    test('handles missing factors gracefully (defaults to 0)', () => {
 60      const factors = {
 61        headline_quality: { score: 10 },
 62        // all others missing
 63      };
 64      // 10 * 0.15 * 10 = 15
 65      assert.equal(computeScoreFromFactors(factors), 15);
 66    });
 67  
 68    test('returns null for null/undefined input', () => {
 69      assert.equal(computeScoreFromFactors(null), null);
 70      assert.equal(computeScoreFromFactors(undefined), null);
 71    });
 72  });
 73  
 74  describe('computeGrade — standard academic scale', () => {
 75    test('A+ for 97-100', () => {
 76      assert.equal(computeGrade(100), 'A+');
 77      assert.equal(computeGrade(97), 'A+');
 78    });
 79  
 80    test('A for 93-96', () => {
 81      assert.equal(computeGrade(96), 'A');
 82      assert.equal(computeGrade(93), 'A');
 83    });
 84  
 85    test('A- for 90-92', () => {
 86      assert.equal(computeGrade(92), 'A-');
 87      assert.equal(computeGrade(90), 'A-');
 88    });
 89  
 90    test('B+ for 87-89', () => {
 91      assert.equal(computeGrade(89), 'B+');
 92      assert.equal(computeGrade(87), 'B+');
 93    });
 94  
 95    test('B for 83-86', () => {
 96      assert.equal(computeGrade(86), 'B');
 97      assert.equal(computeGrade(83), 'B');
 98    });
 99  
100    test('B- for 80-82', () => {
101      assert.equal(computeGrade(82), 'B-');
102      assert.equal(computeGrade(80), 'B-');
103    });
104  
105    test('C+ for 77-79', () => {
106      assert.equal(computeGrade(79), 'C+');
107      assert.equal(computeGrade(77), 'C+');
108    });
109  
110    test('C for 73-76', () => {
111      assert.equal(computeGrade(76), 'C');
112      assert.equal(computeGrade(73), 'C');
113    });
114  
115    test('C- for 70-72', () => {
116      assert.equal(computeGrade(72), 'C-');
117      assert.equal(computeGrade(70), 'C-');
118    });
119  
120    test('D+ for 67-69', () => {
121      assert.equal(computeGrade(69), 'D+');
122      assert.equal(computeGrade(67), 'D+');
123    });
124  
125    test('D for 63-66', () => {
126      assert.equal(computeGrade(66), 'D');
127      assert.equal(computeGrade(63), 'D');
128    });
129  
130    test('D- for 60-62', () => {
131      assert.equal(computeGrade(62), 'D-');
132      assert.equal(computeGrade(60), 'D-');
133    });
134  
135    test('F for 0-59', () => {
136      assert.equal(computeGrade(59), 'F');
137      assert.equal(computeGrade(0), 'F');
138    });
139  
140    test('F for negative scores', () => {
141      assert.equal(computeGrade(-1), 'F');
142      assert.equal(computeGrade(-100), 'F');
143    });
144  
145    test('F for null/undefined', () => {
146      assert.equal(computeGrade(null), 'F');
147      assert.equal(computeGrade(undefined), 'F');
148    });
149  
150    test('proposal threshold boundary: 82 is B- (gets proposals), 83 is B (skipped)', () => {
151      assert.equal(computeGrade(82), 'B-');
152      assert.equal(computeGrade(83), 'B');
153    });
154  });