'use strict'; /** * MATCHUP (platoon) axis — 2026-08-02. * * It was wired and firing on 0/634 prod rows. Three separate absences kept it * silent: oppPitcherByTeam 0, handById 0, bats 0/120. These lock the joins that * fixed each, and the honest-absent behaviour when any one is missing. */ const env = require('../../src/services/environmentContext'); const { adjust } = require('../../src/services/challengerProjection'); const GAME = { teams: { home: { team: { name: 'Philadelphia Phillies' }, probablePitcher: { id: 111 } }, away: { team: { name: 'New York Mets' }, probablePitcher: { id: 222 } }, }, }; const SCHED = { dates: [{ games: [GAME] }] }; const PEOPLE = { people: [{ id: 111, pitchHand: { code: 'L' } }, { id: 222, pitchHand: { code: 'R' } }] }; const PLAYERS = { people: [{ id: 900, batSide: { code: 'L' } }, { id: 901, batSide: { code: 'S' } }] }; const deps = (over = {}) => ({ origin: '', schedule: { games: [] }, pitchers: { games: [] }, // self-origin route returns nothing, as in prod probableSchedule: SCHED, people: PEOPLE, seasonPlayers: PLAYERS, fetchJson: async () => { throw new Error('no network in tests'); }, ...over, }); describe('buildContext — the three joins that were absent', () => { it('falls back to the statsapi probable-pitcher hydrate when the self-origin route is empty', async () => { const ctx = await env.buildContext('mlb', deps()); expect(ctx._internals.oppPitcherByTeam.get('PHI')).toBe(222); // opposing = away SP expect(ctx._internals.oppPitcherByTeam.get('NYM')).toBe(111); expect(ctx.stats.opp_declared).toBe(2); }); it('resolves pitcher handedness for the declared starters', async () => { const ctx = await env.buildContext('mlb', deps()); expect(ctx._internals.handById.get(222)).toBe('R'); expect(ctx.stats.pitchers_with_hand).toBe(2); }); it('joins BATTER handedness off the cached season player list', async () => { const ctx = await env.buildContext('mlb', deps()); expect(ctx._internals.batsById.get(900)).toBe('L'); expect(ctx._internals.batsById.get(901)).toBe('S'); // switch-hitters preserved expect(ctx.stats.batters_with_hand).toBe(2); }); it('ABSTAINS — no starter, no hand, or no batter hand produces NO matchup', async () => { const noSp = await env.buildContext('mlb', deps({ probableSchedule: { dates: [] } })); const r1 = await noSp.contextFor({ stat_type: 'hits', team: 'PHI', playerId: 900 }); expect(r1.matchup).toBeNull(); const ctx = await env.buildContext('mlb', deps()); // known team + known starter, but the batter's hand is unknown const r2 = await ctx.contextFor({ stat_type: 'hits', team: 'PHI', playerId: 999 }); expect(r2.matchup).toBeNull(); }); }); describe('the fallback ladder is LABELLED, and never invents a higher rung', () => { it('a firing matchup carries its tier through to the challenger adjustment', () => { const r = adjust({ pWin: 0.55, direction: 'over', statType: 'hits', matchup: { multiplier: 0.95, label: 'PLATOON', tier: 'batter_own_split', batter_hand: 'L', pitcher_hand: 'R' }, }); const m = r.adjustments.find((a) => a.axis === 'matchup'); expect(m.tier).toBe('batter_own_split'); expect(r.p_win_challenger).toBeLessThan(0.55); }); it('a neutral multiplier produces NO adjustment — never a fabricated zero-nudge row', () => { const r = adjust({ pWin: 0.55, direction: 'over', statType: 'hits', matchup: { multiplier: 1, label: 'PLATOON', tier: 'batter_own_split' }, }); expect(r.adjustments.find((a) => a.axis === 'matchup')).toBeUndefined(); }); });