'use strict'; // Hero rule v2 (Model Train, step 5): highest ev_pct among reads passing the // TAKEABLE gate, A/B grades only. Empty slate → most recent real read. const { pickHeroProp } = require('../../src/services/heroPropService'); function cacheFrom(map) { return async (key) => (key in map ? map[key] : null); } // A graded read carries ev_pct + book_odds (the v2 ranking inputs). const grade = (o) => ({ player_name: o.player, stat_type: o.stat || 'hits', line: o.line ?? 1.5, projection: o.proj ?? 2.0, direction: o.dir || 'over', grade: o.grade, book: o.book || 'dk', ev_pct: o.ev, book_odds: o.odds ?? -120, value: o.value ?? null, gradedAt: { line: o.line ?? 1.5, odds: o.odds ?? -120, timestamp: o.ts || '2026-07-17T19:00:00Z' }, }); describe('pickHeroProp — v2 (EV among takeable A/B)', () => { test('picks the HIGHEST ev_pct among takeable A/B reads', async () => { const cacheGet = cacheFrom({ 'snapshot:mlb:latest': { grades: [ grade({ player: 'LowEV', grade: 'A', ev: 3.1, odds: -120 }), grade({ player: 'HighEV', grade: 'B', ev: 8.4, odds: +110 }), ] } }); const hero = await pickHeroProp({ cacheGet, sports: ['mlb'] }); expect(hero.available).toBe(true); expect(hero.player).toBe('HighEV'); expect(hero.ev_pct).toBe(8.4); }); test('a HUGE EV on an un-takeable price (-900) is NOT the hero (trivia, not opportunity)', async () => { const cacheGet = cacheFrom({ 'snapshot:mlb:latest': { grades: [ grade({ player: 'Chalk', grade: 'A', ev: 20, odds: -900 }), // un-takeable grade({ player: 'Takeable', grade: 'B', ev: 5, odds: -130 }), // in band ] } }); const hero = await pickHeroProp({ cacheGet, sports: ['mlb'] }); expect(hero.player).toBe('Takeable'); }); test('C/D/F grades are ineligible even with high EV', async () => { const cacheGet = cacheFrom({ 'snapshot:mlb:latest': { grades: [ grade({ player: 'HighEV_C', grade: 'C', ev: 12, odds: -110 }), grade({ player: 'RealA', grade: 'A', ev: 4, odds: -110 }), ] } }); const hero = await pickHeroProp({ cacheGet, sports: ['mlb'] }); expect(hero.player).toBe('RealA'); }); test('exposes the value triplet + ev/value on the hero', async () => { const cacheGet = cacheFrom({ 'snapshot:mlb:latest': { grades: [ { player_name: 'Trip', stat_type: 'hits', line: 1.5, projection: 2.1, direction: 'over', grade: 'A', book: 'dk', ev_pct: 6.2, value: true, takeable: true, book_odds: -145, fair_odds: -132, model_odds: -110, gradedAt: { line: 1.5, odds: -145, timestamp: '2026-07-17T19:00:00Z' } }, ] } }); const hero = await pickHeroProp({ cacheGet, sports: ['mlb'] }); expect(hero.book_odds).toBe(-145); expect(hero.fair_odds).toBe(-132); expect(hero.model_odds).toBe(-110); expect(hero.ev_pct).toBe(6.2); expect(hero.value).toBe(true); }); test('no takeable A/B EV read → most recent real read (fallback)', async () => { const cacheGet = cacheFrom({ 'snapshot:mlb:latest': { grades: [ grade({ player: 'OnlyChalk', grade: 'A', ev: 9, odds: -800, ts: '2026-07-17T19:00:00Z' }), ] } }); const hero = await pickHeroProp({ cacheGet, sports: ['mlb'] }); expect(hero.available).toBe(true); expect(hero.is_recent).toBe(true); expect(hero.player).toBe('OnlyChalk'); }); test('nothing cached → { available:false }', async () => { const hero = await pickHeroProp({ cacheGet: cacheFrom({}), sports: ['mlb'] }); expect(hero).toEqual({ available: false }); }); });