Session 23: All-day intelligence layer — schedule, game lines, streaks, hot lists, stat filtering, ParlayAPI dead (1567 tests)

This commit is contained in:
Kev
2026-06-12 11:16:58 -04:00
parent 6ab49d4c37
commit 0538205fab
32 changed files with 2276 additions and 2 deletions
+60
View File
@@ -0,0 +1,60 @@
// Unit: rosterLogs loader (Session 23). Redis-only; no network.
const store = {};
const mockScan = jest.fn();
jest.mock('../../src/utils/redis', () => ({
cacheGet: jest.fn(async (k) => (k in store ? store[k] : null)),
getRedisClient: () => ({ scan: mockScan }),
isDegraded: () => false,
}));
const { loadRosterLogs, __internals } = require('../../src/services/rosterLogs');
beforeEach(() => {
for (const k of Object.keys(store)) delete store[k];
mockScan.mockReset();
});
describe('rosterLogs', () => {
test('fast path: returns a prefetched roster blob without scanning', async () => {
store['rosterlogs:nba'] = [{ name: 'Wemby', games: [{ points: 30 }] }];
const roster = await loadRosterLogs('nba');
expect(roster).toHaveLength(1);
expect(mockScan).not.toHaveBeenCalled();
});
test('scan path: assembles roster from per-player gamelogs keys', async () => {
store['gamelogs:nba:Wembanyama:20'] = [{ points: 30, team: 'SA', playerId: 'W1' }];
store['gamelogs:nba:Brunson:20'] = [{ points: 24, team: 'NYK' }];
mockScan
.mockResolvedValueOnce(['0', ['gamelogs:nba:Wembanyama:20', 'gamelogs:nba:Brunson:20']]);
const roster = await loadRosterLogs('nba');
expect(roster.map((p) => p.name).sort()).toEqual(['Brunson', 'Wembanyama']);
const wemby = roster.find((p) => p.name === 'Wembanyama');
expect(wemby.team).toBe('SA');
expect(wemby.playerId).toBe('W1');
});
test('dedupes by highest game-count variant', async () => {
store['gamelogs:nba:Star:10'] = [{ points: 1 }, { points: 2 }];
store['gamelogs:nba:Star:20'] = [{ points: 1 }, { points: 2 }, { points: 3 }];
mockScan.mockResolvedValueOnce(['0', ['gamelogs:nba:Star:10', 'gamelogs:nba:Star:20']]);
const roster = await loadRosterLogs('nba');
expect(roster).toHaveLength(1);
expect(roster[0].games).toHaveLength(3); // the :20 variant wins
});
test('empty cache → empty roster, never throws', async () => {
mockScan.mockResolvedValueOnce(['0', []]);
expect(await loadRosterLogs('nba')).toEqual([]);
});
test('scan failure → returns what it had, no throw', async () => {
mockScan.mockRejectedValueOnce(new Error('redis down'));
expect(await loadRosterLogs('nba')).toEqual([]);
});
test('playerFromKey parses the player name out of the key', () => {
expect(__internals.playerFromKey('gamelogs:nba:LeBron James:20', 'nba')).toBe('LeBron James');
});
});