Session 7e: Grade adapter, normalize consolidation, ARCH-2 banners
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
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');
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user