Files
vyndr/tests/unit/gradeAdapter.test.js
T

205 lines
7.3 KiB
JavaScript

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');
}
});
});