/* ============================================================ Session 77 — WIRING the dormant adjusters. Pure input-wiring; the adjusters' internal logic is not touched. Proves environment + matchup reach the grade. ============================================================ */ const ec = require('../../src/services/environmentContext'); const ch = require('../../src/services/challengerProjection'); const SCHEDULE = { games: [ { homeTeam: { abbreviation: 'COL' }, awayTeam: { abbreviation: 'LAD' }, gameTime: '2026-07-21T20:10Z', venue: 'Coors Field' }, { homeTeam: { abbreviation: 'TB' }, awayTeam: { abbreviation: 'NYY' }, gameTime: '2026-07-21T23:05Z', venue: 'Tropicana Field' }, ] }; const PITCHERS = { games: [ { home: { team: 'Colorado Rockies', pitcherId: 111 }, away: { team: 'Los Angeles Dodgers', pitcherId: 222 } }, { home: { team: 'Tampa Bay Rays', pitcherId: 333 }, away: { team: 'New York Yankees', pitcherId: 444 } }, ] }; const PEOPLE = { people: [ { id: 111, pitchHand: { code: 'R' } }, { id: 222, pitchHand: { code: 'L' } }, { id: 333, pitchHand: { code: 'R' } }, { id: 444, pitchHand: { code: 'R' } }, ] }; // A Coors hitter (LAD) facing the Rockies' RHP; deep vs-RHP power split. const SPLITS_LAD = { stats: [{ splits: [ { split: { code: 'vl' }, stat: { plateAppearances: 120, avg: '.250', slg: '.420' } }, { split: { code: 'vr' }, stat: { plateAppearances: 420, avg: '.250', slg: '.560' } }, ] }] }; // Route each URL to its fixture. const fetchJson = async (url) => { if (url.includes('/pitchers')) return PITCHERS; if (url.includes('/api/schedule/mlb')) return SCHEDULE; if (url.includes('statsapi') && url.includes('/schedule')) return SCHEDULE; if (url.includes('personIds=')) return PEOPLE; if (url.includes('open-meteo')) return { hourly: { time: ['2026-07-21T20:00'], temperature_2m: [88], wind_speed_10m: [14], wind_direction_10m: [185], precipitation_probability: [0], } }; if (url.includes('statSplits')) return SPLITS_LAD; return {}; }; async function build() { return ec.buildContext('mlb', { fetchJson, schedule: SCHEDULE, pitchers: PITCHERS, people: PEOPLE }); } describe('team resolution', () => { it('maps full names and abbreviations to the coord key', () => { expect(ec.abbrOf('Los Angeles Dodgers')).toBe('LAD'); expect(ec.abbrOf('COL')).toBe('COL'); expect(ec.abbrOf('Nobody')).toBeNull(); expect(ec.abbrOf(null)).toBeNull(); }); it('has coordinates for all 30 parks', () => { expect(Object.keys(ec.PARK_COORDS)).toHaveLength(30); }); }); describe('environment reaches the grade', () => { it('a Coors prop gets env_multiplier > 1 (park × weather)', async () => { const ctx = await build(); const c = await ctx.contextFor({ team: 'LAD', stat_type: 'home_runs', direction: 'over', playerId: 9, bats: 'R' }); expect(c.environment).not.toBeNull(); expect(c.environment.multiplier).toBeGreaterThan(1); expect(c.environment.park_base).toBeGreaterThan(1); // Coors expect(c.environment.weather_mod).toBeGreaterThan(1); // wind out-ish, warm }); it('a DOME prop keeps its park factor but weather stands down', async () => { const ctx = await build(); const c = await ctx.contextFor({ team: 'NYY', stat_type: 'home_runs', direction: 'over', playerId: 8, bats: 'R' }); // Tampa's park factor may compose to != 1, but the weather mod is neutral. if (c.environment) { expect(c.environment.weather_mod).toBe(1); expect(c.environment.weather_state).toBe('dome_na'); } }); it('a prop for a team not on the slate gets NO environment', async () => { const ctx = await build(); const c = await ctx.contextFor({ team: 'SEA', stat_type: 'home_runs', direction: 'over', playerId: 7, bats: 'R' }); expect(c.environment).toBeNull(); }); it('never fabricates a venue — a grade with no team is absent', async () => { const ctx = await build(); const c = await ctx.contextFor({ team: null, stat_type: 'home_runs', direction: 'over', playerId: 6, bats: 'R' }); expect(c.environment).toBeNull(); }); }); describe('matchup (platoon) reaches the grade', () => { it('fires with a declared opposing pitcher + known hitter hand', async () => { const ctx = await build(); // LAD hitter, opposing pitcher 111 is RHP → uses the deep vs-RHP power split. const c = await ctx.contextFor({ team: 'LAD', stat_type: 'home_runs', direction: 'over', playerId: 9, bats: 'R' }); expect(c.matchup).not.toBeNull(); expect(c.matchup.pitcher_hand).toBe('R'); expect(c.matchup.observed_pa).toBe(420); }); it('honest-absents when the hitter hand is unknown', async () => { const ctx = await build(); const c = await ctx.contextFor({ team: 'LAD', stat_type: 'home_runs', direction: 'over', playerId: 9, bats: null }); expect(c.matchup).toBeNull(); }); it('honest-absents when no opposing pitcher is declared', async () => { const ctx = await ec.buildContext('mlb', { fetchJson, schedule: SCHEDULE, pitchers: { games: [] }, people: PEOPLE }); const c = await ctx.contextFor({ team: 'LAD', stat_type: 'home_runs', direction: 'over', playerId: 9, bats: 'R' }); expect(c.matchup).toBeNull(); }); }); describe('non-MLB honest-absents entirely', () => { it('wnba returns no environment or matchup', async () => { const ctx = await ec.buildContext('wnba', { fetchJson }); const c = await ctx.contextFor({ team: 'LV', stat_type: 'points', direction: 'over' }); expect(c.environment).toBeNull(); expect(c.matchup).toBeNull(); }); }); describe('the combined adjustment stays BOUNDED', () => { it('archetype + environment + platoon do not compound into an over-adjust', async () => { const axes = require('../../src/services/archetypeAxes'); const slugger = axes.classifyPlayer({ role: 'batter', sample_pa: 400, barrel_pct: 20, k_pct: 20, chase_pct: 30, avg_launch_angle: 18, sweet_spot_pct: 40 }); // Coors + wind-out + a favourable platoon, all at once, on a slugger. const env = { multiplier: 1.28, label: 'PARK × WEATHER', park_base: 1.15, weather_mod: 1.11 }; const matchup = { multiplier: 1.14, label: 'PLATOON', pitcher_hand: 'R' }; const r = ch.adjust({ pWin: 0.5, direction: 'over', statType: 'home_runs', classification: slugger, environment: env, matchup }); // Every layer is capped, and the total nudge is clamped — the combined move // must stay a lean, not a 40% swing. expect(Math.abs(r.delta)).toBeLessThan(0.12); // and all three are independently recorded for attribution const axesSeen = r.adjustments.map((a) => a.axis); expect(axesSeen).toContain('environment'); expect(axesSeen).toContain('matchup'); }); }); describe('async attachChallenger still preserves the champion', () => { it('passes environment + matchup through, p_win untouched', async () => { const out = await ch.attachChallenger( [{ player: 'X', stat_type: 'home_runs', direction: 'over', p_win: 0.5 }], () => null, async () => ({ environment: { multiplier: 1.2, park_base: 1.2, weather_mod: 1, weather_state: 'present' }, matchup: null }), ); expect(out[0].p_win).toBe(0.5); // champion untouched expect(out[0].env_multiplier).toBe(1.2); // retained attributably expect(out[0].p_win_challenger).toBeGreaterThan(0.5); }); });