// Unit: streaks engine (Session 23). Pure function — no mocks needed. const { computeStreaks, computePlayerStreaks } = require('../../src/services/streaksService'); // Most-recent-first game logs. function nbaGames(...rows) { return rows; } describe('streaksService — NBA', () => { test('detects a consecutive 25+ points streak from the latest games', () => { const player = { name: 'Wembanyama', playerId: 'W1', team: 'SA', games: [{ points: 31 }, { points: 28 }, { points: 33 }, { points: 22 }, { points: 40 }], }; const streaks = computePlayerStreaks(player, 'nba'); const pts = streaks.find((s) => s.category === 'points'); expect(pts).toBeDefined(); // 31,28,33 meet 25+, then 22 breaks → run of 3. expect(pts.currentStreak).toBe(3); expect(pts.description).toBe('3-game 25+ pts streak'); }); test('streak breaks immediately when the latest game misses the threshold', () => { const player = { name: 'X', games: [{ points: 10 }, { points: 30 }, { points: 30 }] }; const streaks = computePlayerStreaks(player, 'nba'); expect(streaks.find((s) => s.category === 'points')).toBeUndefined(); // run of 0 < MIN_STREAK }); test('only the strongest streak per category surfaces', () => { // 20+ and 25+ both qualify; keep the 25+ (higher run not guaranteed, but // one points entry only). const player = { name: 'Y', games: [{ points: 26 }, { points: 27 }, { points: 21 }] }; const streaks = computePlayerStreaks(player, 'nba'); const ptsEntries = streaks.filter((s) => s.category === 'points'); expect(ptsEntries).toHaveLength(1); }); test('double-double streak counts categories >= 10', () => { const player = { name: 'Jokic', games: [ { points: 20, rebounds: 12, assists: 11 }, { points: 15, rebounds: 10, assists: 9 }, { points: 8, rebounds: 11, assists: 3 }, ], }; const streaks = computePlayerStreaks(player, 'nba'); const dd = streaks.find((s) => s.type === 'double_double'); expect(dd).toBeDefined(); // g0: 3 doubles, g1: 2 doubles, g2: 1 double → dd (>=2) run breaks at g2. expect(dd.currentStreak).toBe(2); expect(dd.description).toBe('2-game double-double streak'); }); test('empty game logs → empty streaks (not error)', () => { expect(computePlayerStreaks({ name: 'Z', games: [] }, 'nba')).toEqual([]); expect(computeStreaks([], 'nba')).toEqual([]); }); test('chronological input is reversed before counting', () => { const player = { name: 'C', games: [{ points: 22 }, { points: 30 }, { points: 31 }] }; // As chronological (oldest first) the latest is 31,30 → run 2. const streaks = computePlayerStreaks(player, 'nba', { chronological: true }); const pts = streaks.find((s) => s.category === 'points'); expect(pts.currentStreak).toBe(2); }); }); describe('streaksService — MLB', () => { test('classic hit streak counts consecutive games with a hit', () => { const player = { name: 'Acuna', team: 'ATL', games: [{ hits: 2 }, { hits: 1 }, { hits: 3 }, { hits: 0 }, { hits: 1 }], }; const streaks = computePlayerStreaks(player, 'mlb'); const hit = streaks.find((s) => s.type === 'hit_streak'); expect(hit.currentStreak).toBe(3); expect(hit.description).toBe('3-game hit streak'); }); test('quality-start streak uses IP + ER', () => { const pitcher = { name: 'Strider', games: [ { inningsPitched: 7, earnedRuns: 2 }, { inningsPitched: 6, earnedRuns: 3 }, { inningsPitched: 5, earnedRuns: 1 }, // < 6 IP breaks it ], }; const streaks = computePlayerStreaks(pitcher, 'mlb'); const qs = streaks.find((s) => s.type === 'qs_streak'); expect(qs.currentStreak).toBe(2); }); test('MLB specs do not produce NBA streak types', () => { const player = { name: 'P', games: [{ hits: 2 }, { hits: 2 }] }; const streaks = computePlayerStreaks(player, 'mlb'); expect(streaks.every((s) => s.type !== 'points_25')).toBe(true); }); }); describe('streaksService — fan-out + filtering', () => { const roster = [ { name: 'A', games: [{ points: 30 }, { points: 30 }, { points: 30 }] }, { name: 'B', games: [{ assists: 9 }, { assists: 8 }] }, { name: 'C', games: [{ points: 26 }, { points: 27 }] }, ]; test('sorts by streak length descending', () => { const streaks = computeStreaks(roster, 'nba'); expect(streaks[0].player).toBe('A'); // 3-game points streak leads expect(streaks[0].currentStreak).toBe(3); }); test('stat filter narrows to a single category', () => { const streaks = computeStreaks(roster, 'nba', { stat: 'points' }); expect(streaks.every((s) => s.category === 'points')).toBe(true); expect(streaks.map((s) => s.player).sort()).toEqual(['A', 'C']); }); test('limit caps the result', () => { const streaks = computeStreaks(roster, 'nba', { limit: 1 }); expect(streaks).toHaveLength(1); }); test('all filter (default) returns every category', () => { const streaks = computeStreaks(roster, 'nba', { stat: 'all' }); const cats = new Set(streaks.map((s) => s.category)); expect(cats.has('points')).toBe(true); expect(cats.has('assists')).toBe(true); }); });