// Session 51 — Team Hub service. Adapter + cache injected (no network). const svc = require('../../src/services/teamService'); function memCache(initial) { const store = { ...(initial || {}) }; return { store, cacheGet: async (k) => (k in store ? store[k] : null), cacheSet: async (k, v) => { store[k] = v; } }; } const judgeSeason = { homeRuns: 34, atBats: 330, gamesPlayed: 92, avg: '.288', ops: '1.012', rbi: 87 }; const acePitcherSeason = { era: '2.4', strikeOuts: 130, inningsPitched: '110.0', whip: '0.92', gamesStarted: 17, strikeoutsPer9Inn: '10.6' }; const mlbAdapter = { async resolveTeam(abbr) { return abbr === 'NYY' ? { id: 147, abbr: 'NYY', name: 'New York Yankees' } : null; }, async getTeamRoster() { return [ { id: 592450, name: 'Aaron Judge', position: 'RF' }, { id: 1, name: 'Gerrit Cole', position: 'P' }, { id: 2, name: 'Bench Guy', position: '2B' }, ]; }, async getSeasonAverages(id, _s, group) { if (id === 592450) return judgeSeason; if (id === 1 && group === 'pitching') return acePitcherSeason; return null; // bench guy: no stats }, }; const gradesEnv = { grades: [ { player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, direction: 'over', grade: 'A', archetype: 'BOMBER', gradedAt: { line: 1.5, timestamp: '2026-06-19T18:00:00Z' } }, { player: 'Aaron Judge', stat_type: 'home_runs', line: 0.5, direction: 'over', grade: 'C', archetype: 'BOMBER' }, ], }; describe('getTeamHub (MLB, injected)', () => { it('returns team name + roster with archetype, stats, props', async () => { const cache = memCache({ 'grades:mlb': gradesEnv }); const hub = await svc.getTeamHub('mlb', 'nyy', { mlbAdapter, cacheGet: cache.cacheGet, cacheSet: cache.cacheSet }); expect(hub.team.name).toBe('New York Yankees'); expect(hub.team.abbr).toBe('NYY'); expect(hub.roster).toHaveLength(3); const judge = hub.roster.find((p) => p.player === 'Aaron Judge'); expect(judge.archetype.primary).toBe('BOMBER'); // from snapshot expect(judge.stats.length).toBeGreaterThan(0); expect(judge.propCount).toBe(2); const cole = hub.roster.find((p) => p.player === 'Gerrit Cole'); expect(cole.archetype.primary).toBe('ALPHA'); // classified from pitching stats }); it('shows a player with no stats + no props gracefully', async () => { const cache = memCache({ 'grades:mlb': gradesEnv }); const hub = await svc.getTeamHub('mlb', 'NYY', { mlbAdapter, cacheGet: cache.cacheGet, cacheSet: cache.cacheSet }); const bench = hub.roster.find((p) => p.player === 'Bench Guy'); expect(bench.propCount).toBe(0); expect(bench.archetype).toBeNull(); }); it('returns null for an unknown team (→ 404)', async () => { const cache = memCache(); expect(await svc.getTeamHub('mlb', 'ZZZ', { mlbAdapter, cacheGet: cache.cacheGet, cacheSet: cache.cacheSet })).toBeNull(); }); it('normalizes roster player display names (no dots) — Session 54', async () => { const dotted = { ...mlbAdapter, async getTeamRoster() { return [{ id: 99, name: 'J.C. Escarra', position: 'C' }]; }, async getSeasonAverages() { return null; }, }; const cache = memCache({ 'grades:mlb': { grades: [] } }); const hub = await svc.getTeamHub('mlb', 'NYY', { mlbAdapter: dotted, cacheGet: cache.cacheGet, cacheSet: cache.cacheSet }); expect(hub.roster[0].player).toBe('JC Escarra'); }); it('caches the assembled hub (writes teamhub:{sport}:{abbr})', async () => { const cache = memCache({ 'grades:mlb': gradesEnv }); await svc.getTeamHub('mlb', 'NYY', { mlbAdapter, cacheGet: cache.cacheGet, cacheSet: cache.cacheSet }); expect(cache.store['teamhub:mlb:NYY']).toBeTruthy(); }); }); describe('getTeamHub (NBA fallback)', () => { it('builds a snapshot roster when no MLB feed', async () => { const cache = memCache({ 'grades:nba': { grades: [{ player: 'Victor Wembanyama', stat_type: 'points', line: 26.5, direction: 'over', grade: 'A', archetype: 'FORTRESS' }] } }); const hub = await svc.getTeamHub('nba', 'SA', { cacheGet: cache.cacheGet, cacheSet: cache.cacheSet }); expect(hub.rosterSource).toBe('snapshot'); expect(hub.roster[0].player).toBe('Victor Wembanyama'); expect(hub.roster[0].archetype.primary).toBe('FORTRESS'); }); });