const { toLegacyShape, __internals: { FOUR_LETTER_MAP, fourLetterGrade, legacyConfidence, killConditionsFromFactors, buildReasoningSummary, legacyEdgePct, }, } = require('../../src/utils/gradeAdapter'); describe('gradeAdapter — fourLetterGrade', () => { test('A-tier (A+, A, A-) all map to "A"', () => { expect(fourLetterGrade('A+')).toBe('A'); expect(fourLetterGrade('A')).toBe('A'); expect(fourLetterGrade('A-')).toBe('A'); }); test('B-tier maps to "B"', () => { expect(fourLetterGrade('B+')).toBe('B'); expect(fourLetterGrade('B')).toBe('B'); expect(fourLetterGrade('B-')).toBe('B'); }); test('C-tier maps to "C"', () => { expect(fourLetterGrade('C+')).toBe('C'); expect(fourLetterGrade('C')).toBe('C'); expect(fourLetterGrade('C-')).toBe('C'); }); test('D maps to "D", F maps to "F" (DemoScan handles F → "—")', () => { expect(fourLetterGrade('D')).toBe('D'); expect(fourLetterGrade('F')).toBe('F'); }); test('unknown / null / non-string returns null', () => { expect(fourLetterGrade('Z')).toBeNull(); expect(fourLetterGrade(null)).toBeNull(); expect(fourLetterGrade(undefined)).toBeNull(); expect(fourLetterGrade(42)).toBeNull(); }); test('FOUR_LETTER_MAP exposes every engine1 grade', () => { const expected = ['A+','A','A-','B+','B','B-','C+','C','C-','D','F']; for (const g of expected) expect(FOUR_LETTER_MAP[g]).toBeDefined(); }); }); describe('gradeAdapter — legacyConfidence', () => { test('0.0 → 0, 0.5 → 50, 1.0 → 100', () => { expect(legacyConfidence(0.0)).toBe(0); expect(legacyConfidence(0.5)).toBe(50); expect(legacyConfidence(1.0)).toBe(100); }); test('0.873 → 87 (rounded)', () => { expect(legacyConfidence(0.873)).toBe(87); }); test('out-of-range clamps to [0, 100]', () => { expect(legacyConfidence(-0.5)).toBe(0); expect(legacyConfidence(2.0)).toBe(100); }); test('non-numeric input returns null', () => { expect(legacyConfidence(null)).toBeNull(); expect(legacyConfidence(undefined)).toBeNull(); expect(legacyConfidence('abc')).toBeNull(); }); }); describe('gradeAdapter — killConditionsFromFactors', () => { test('translates known negative factors to kill condition entries', () => { const out = killConditionsFromFactors(['trap_composite_high', 'l5_cold_vs_line']); const codes = out.map((k) => k.code); expect(codes).toContain('TRAP'); expect(codes).toContain('COLD_L5'); }); test('positive factors are ignored', () => { const out = killConditionsFromFactors(['l5_hot_vs_line', 'home_game', 'rested_2plus']); expect(out).toEqual([]); }); test('deduplicates by code (l5_cold + l5_hot_under both → COLD_L5)', () => { const out = killConditionsFromFactors(['l5_cold_vs_line', 'l5_hot_vs_under']); expect(out).toHaveLength(1); expect(out[0].code).toBe('COLD_L5'); }); test('empty / non-array → []', () => { expect(killConditionsFromFactors([])).toEqual([]); expect(killConditionsFromFactors(null)).toEqual([]); expect(killConditionsFromFactors(undefined)).toEqual([]); }); }); describe('gradeAdapter — buildReasoningSummary', () => { test('uses summaryOverride when provided', () => { const out = buildReasoningSummary({ grade: 'A' }, {}, 'fancy override'); expect(out).toBe('fancy override'); }); test('builds a sentence from top_factors', () => { const out = buildReasoningSummary({ grade: 'A-', top_factors: ['l5_hot_vs_line', 'home_game', 'rested_2plus'], }, { player: 'X' }); expect(out).toContain('A-'); expect(out).toContain('l5_hot_vs_line'); expect(out).toContain('favoring the play'); }); test('falls back gracefully when no factors are surfaced', () => { const out = buildReasoningSummary({ grade: 'B' }); expect(typeof out).toBe('string'); expect(out).toContain('B'); }); }); describe('gradeAdapter — legacyEdgePct', () => { test('numeric override passes through', () => { expect(legacyEdgePct(5.2)).toBe(5.2); expect(legacyEdgePct(-3)).toBe(-3); }); test('non-numeric returns 0 (legacy callers expect the field present)', () => { expect(legacyEdgePct(null)).toBe(0); expect(legacyEdgePct(undefined)).toBe(0); expect(legacyEdgePct('abc')).toBe(0); }); }); describe('gradeAdapter — toLegacyShape (round-trip)', () => { const engine1Result = { grade: 'A-', confidence: 0.78, top_factors: ['l5_hot_vs_line', 'opp_rank_stat', 'rested_2plus'], all_factors: ['l5_hot_vs_line', 'opp_rank_stat', 'rested_2plus', 'home_game'], }; const prop = { player: 'Jalen Brunson', stat_type: 'points', line: 26.5, direction: 'over', book: 'draftkings', sport: 'nba', }; test('produces every field DemoScan consumes', () => { const out = toLegacyShape(engine1Result, prop); expect(out).toHaveProperty('grade', 'A'); expect(out).toHaveProperty('confidence', 78); expect(out).toHaveProperty('reasoning.summary'); expect(typeof out.reasoning.summary).toBe('string'); expect(out).toHaveProperty('kill_conditions_triggered'); expect(Array.isArray(out.kill_conditions_triggered)).toBe(true); expect(out).toHaveProperty('edge_pct'); expect(out).toHaveProperty('player', 'Jalen Brunson'); expect(out).toHaveProperty('stat_type', 'points'); expect(out).toHaveProperty('line', 26.5); expect(out).toHaveProperty('direction', 'over'); expect(out).toHaveProperty('book', 'draftkings'); }); test('edgePct override flows through', () => { const out = toLegacyShape(engine1Result, prop, { edgePct: 4.7 }); expect(out.edge_pct).toBe(4.7); }); test('summaryOverride wins over the factor-based sentence', () => { const out = toLegacyShape(engine1Result, prop, { summaryOverride: 'Hand-written take.' }); expect(out.reasoning.summary).toBe('Hand-written take.'); }); test('partial engine1 input does not crash', () => { const partial = { grade: 'B+' }; const out = toLegacyShape(partial, prop); expect(out.grade).toBe('B'); expect(out.confidence).toBeNull(); expect(out.kill_conditions_triggered).toEqual([]); expect(typeof out.reasoning.summary).toBe('string'); }); test('null engine1 input returns null', () => { expect(toLegacyShape(null, prop)).toBeNull(); expect(toLegacyShape(undefined, prop)).toBeNull(); }); test('engine1_factors travel through reasoning.steps for debugging', () => { const out = toLegacyShape(engine1Result, prop); expect(Array.isArray(out.reasoning.steps.engine1_factors)).toBe(true); expect(out.reasoning.steps.engine1_factors).toEqual(engine1Result.all_factors); expect(out.reasoning.steps.final_grade).toBe('A'); }); test('legacy-shape compatibility: every key DemoScan reads is present', () => { const out = toLegacyShape(engine1Result, prop); // DemoScan reads: grade, confidence, reasoning.summary, // kill_conditions_triggered (with .code on each entry), // implied_probability (optional — adapter doesn't add it but doesn't // need to since the field is allowed to be undefined). expect(out.grade).toBeDefined(); expect(out.confidence).toBeDefined(); expect(out.reasoning.summary).toBeDefined(); for (const kc of out.kill_conditions_triggered) { expect(kc.code).toBeDefined(); expect(typeof kc.code).toBe('string'); } }); });