// Session 43 — real-stats wiring into playerIntelService. Adapters are injected // so these run pure (no statsapi.mlb.com, no Python NBA service). const svc = require('../../src/services/playerIntelService'); // A fake mlbStatsAdapter.getPlayerStats returning a power-pull hitter line. const judgeAdapter = { async getPlayerStats() { return { found: true, id: 592450, name: 'Aaron Judge', team: 'New York Yankees', position: 'RF', group: 'hitting', season: { avg: '.288', homeRuns: 34, rbi: 87, ops: '1.012', gamesPlayed: 92, stolenBases: 9, runs: 80, doubles: 18, strikeOuts: 120, plateAppearances: 400, atBats: 330 }, last10: [ { date: '2026-06-15', opponent: 'Boston Red Sox', stat: { hits: 2, atBats: 4, homeRuns: 1 } }, { date: '2026-06-16', opponent: 'Tampa Bay Rays', stat: { hits: 1, atBats: 3, homeRuns: 0 } }, ], }; }, }; const aceAdapter = { async getPlayerStats() { return { found: true, id: 1, name: 'Tarik Skubal', team: 'Detroit Tigers', position: 'P', group: 'pitching', season: { era: '2.41', strikeOuts: 130, inningsPitched: '110.0', whip: '0.92', gamesStarted: 17, strikeoutsPer9Inn: '10.6', saves: 0 }, last10: [{ date: '2026-06-14', opponent: 'Chicago White Sox', stat: { inningsPitched: '7.0', strikeOuts: 9 } }], }; }, }; describe('resolvePlayerStats (MLB, injected adapter)', () => { it('normalizes a hitter into classifier input + display rows + last10', async () => { const r = await svc.resolvePlayerStats('Aaron Judge', 'mlb', { mlbAdapter: judgeAdapter }); expect(r.found).toBe(true); expect(r.team).toBe('New York Yankees'); expect(r.classifierInput.hr).toBe(34); expect(r.classifierInput.k_rate).toBeGreaterThan(0); expect(r.season.find((s) => s.k === 'HR').v).toBe('34'); expect(r.season.find((s) => s.k === 'AVG').v).toBe('.288'); expect(r.last10.length).toBe(2); expect(r.last10[0].stat).toContain('HR'); // most-recent-first summary }); it('normalizes a pitcher (group=pitching) into ERA/K9/role', async () => { const r = await svc.resolvePlayerStats('Tarik Skubal', 'mlb', { mlbAdapter: aceAdapter }); expect(r.classifierInput.era).toBeCloseTo(2.41, 2); expect(r.classifierInput.k9).toBeCloseTo(10.6, 1); expect(r.classifierInput.role).toBe('SP'); expect(r.season.find((s) => s.k === 'ERA').v).toBe('2.41'); }); it('returns found:false when the adapter has no data', async () => { const r = await svc.resolvePlayerStats('Nobody', 'mlb', { mlbAdapter: { async getPlayerStats() { return { found: false }; } } }); expect(r.found).toBe(false); }); }); describe('getPlayerIntel with real stats (Session 43)', () => { it('returns found:true + real season + an archetype classified from real stats', async () => { const r = await svc.getPlayerIntel('Aaron Judge', 'mlb', { cacheGet: async () => null, resolveStats: (name, sport) => svc.resolvePlayerStats(name, sport, { mlbAdapter: judgeAdapter }), }); expect(r.found).toBe(true); expect(r.team).toBe('New York Yankees'); expect(r.season.length).toBeGreaterThan(0); // 34 HR + high K-rate → POWER PULL, not the empty-stats fallback. expect(['POWER PULL', 'POWER SLUGGER', 'RUN PRODUCER']).toContain(r.archetype.primary.name); expect(r.archetype.primary.name).not.toBe('UTILITY PLAYER'); }); it('classifies an ace pitcher from real stats', async () => { const r = await svc.getPlayerIntel('Tarik Skubal', 'mlb', { cacheGet: async () => null, resolveStats: (name, sport) => svc.resolvePlayerStats(name, sport, { mlbAdapter: aceAdapter }), }); expect(r.archetype.primary.name).toBe('ACE'); expect(r.found).toBe(true); }); it('still degrades gracefully when no stats and no props (found:false)', async () => { const r = await svc.getPlayerIntel('Ghost Player', 'mlb', { cacheGet: async () => null, resolveStats: async () => ({ found: false }), }); expect(r.found).toBe(false); expect(r.archetype.primary).toBeTruthy(); // fallback archetype still present expect(r.season).toEqual([]); }); });