// Wave 0 — THE PROOF: an NBA/WNBA prop now GRADES instead of refusing. // // Root cause (pre-fix): featureCache's non-MLB branch called gameLogService // (Python nba_api, offline in prod) → null logs → no l5/l20 → projectionFor // null → insufficientDataResult → the whole slate refused. This test injects // the Python source as NULL (offline) and the ESPN gamelog as the free fallback, // and asserts real l5_avg/l20_avg emerge → projectionFor returns a projection. // Python game-log service is OFFLINE (returns null) — the prod failure mode. jest.mock('../../src/services/intelligence/gameLogService', () => ({ getGameLogs: async () => null, getCareerPlayoffGames: async () => null, getWithWithoutStats: async () => null, })); // ESPN gamelog fallback — a normalized result (as espnStatsAdapter would return). const espnLog = { res: null }; jest.mock('../../src/services/adapters/espnStatsAdapter', () => ({ getPlayerGameLog: async () => espnLog.res, })); const { __internals: fc } = require('../../src/services/intelligence/featureCache'); const { __internals: eng } = require('../../src/services/intelligence/analyzeViaEngine1'); // Most-recent first, matching getPlayerGameLog's contract. const lebronLog = { found: true, id: '1966', last10: [ { date: '2026-01-05T00:30:00Z', stat: { points: 24, rebounds: 12, assists: 3, threes: 2, minutes: 40, pra: 39 } }, { date: '2026-01-03T00:30:00Z', stat: { points: 28, rebounds: 10, assists: 7, threes: 3, minutes: 36, pra: 45 } }, { date: '2026-01-01T00:30:00Z', stat: { points: 23, rebounds: 8, assists: 5, threes: 1, minutes: 32, pra: 36 } }, { date: '2025-12-29T00:30:00Z', stat: { points: 30, rebounds: 9, assists: 6, threes: 4, minutes: 38, pra: 45 } }, { date: '2025-12-27T00:30:00Z', stat: { points: 21, rebounds: 11, assists: 8, threes: 2, minutes: 34, pra: 40 } }, { date: '2025-12-25T00:30:00Z', stat: { points: 26, rebounds: 7, assists: 9, threes: 3, minutes: 37, pra: 42 } }, ], }; describe('nbaGameLogFeatures (pure)', () => { it('derives l5/l10/l20 + rest + usage for an NBA points prop', () => { const f = fc.nbaGameLogFeatures(lebronLog, 'points'); // l5 = mean of the 5 most-recent points (24,28,23,30,21) = 25.2 expect(f.l5_avg).toBeCloseTo((24 + 28 + 23 + 30 + 21) / 5, 5); expect(f.l20_avg).toBeGreaterThan(0); expect(f.minutes_per_game).toBeGreaterThan(0); // consecutive games (2d apart) → 1d gap → 0 rest? dates are 2d apart → gap 2 → rest 1 expect(f.rest_days).toBe(1); }); it('handles combo (pra) + threes via the log-field map', () => { expect(fc.nbaGameLogFeatures(lebronLog, 'pra').l5_avg).toBeGreaterThan(0); expect(fc.nbaGameLogFeatures(lebronLog, 'threes').l5_avg).toBeGreaterThan(0); // pts_reb_ast combo sums components → equals the precomputed pra expect(fc.nbaGameLogFeatures(lebronLog, 'pts_reb_ast').l5_avg) .toBeCloseTo(fc.nbaGameLogFeatures(lebronLog, 'pra').l5_avg, 5); }); it('returns {} for an unfound player or unmapped stat', () => { expect(fc.nbaGameLogFeatures({ found: false }, 'points')).toEqual({}); expect(fc.nbaGameLogFeatures(lebronLog, 'not_a_stat')).toEqual({}); }); }); describe('gameLogFeatures — ESPN fallback when Python is offline', () => { it('emits non-null l5_avg/l20_avg for nba (Python null → ESPN fallback)', async () => { espnLog.res = lebronLog; const f = await fc.gameLogFeatures('LeBron James', 'nba', 'points'); expect(f.l5_avg).not.toBeNull(); expect(f.l5_avg).toBeGreaterThan(0); expect(f.l20_avg).toBeGreaterThan(0); }); it('works for wnba too', async () => { espnLog.res = lebronLog; // shape-identical const f = await fc.gameLogFeatures("A'ja Wilson", 'wnba', 'rebounds'); expect(f.l5_avg).toBeGreaterThan(0); }); it('degrades to {} when ESPN also has nothing (still no fabrication)', async () => { espnLog.res = { found: false }; const f = await fc.gameLogFeatures('Ghost Player', 'nba', 'points'); expect(f).toEqual({}); }); }); describe('THE UNLOCK — projectionFor now returns a grade, not insufficient_data', () => { it('empty features (the old prod state) → projection null → REFUSE', () => { expect(eng.projectionFor({}, { stat_type: 'points', line: 20 })).toBeNull(); }); it('ESPN-derived features → finite projection → the prop GRADES', async () => { espnLog.res = lebronLog; const features = await fc.gameLogFeatures('LeBron James', 'nba', 'points'); const projection = eng.projectionFor(features, { stat_type: 'points', line: 24.5 }); expect(projection).not.toBeNull(); expect(Number.isFinite(projection)).toBe(true); // and the intel card lights up off the same vector const intel = eng.buildIntelFields(features); expect(intel.season_avg).toBeDefined(); expect(intel.last10_avg).toBeDefined(); expect(intel.form).toBeDefined(); }); });