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
+82
View File
@@ -0,0 +1,82 @@
// Unit: hot-list engine (Session 23). Pure function.
const { computeHotList } = require('../../src/services/hotListService');
describe('hotListService', () => {
test('"hot" means above personal average, not just high raw numbers', () => {
const players = [
// 20-PPG player erupting for 28/31/25 → HOT
{ name: 'Riser', games: [{ points: 28 }, { points: 31 }, { points: 25 }, { points: 20 }, { points: 19 }, { points: 21 }, { points: 20 }, { points: 18 }, { points: 22 }, { points: 20 }] },
// 30-PPG star who dropped 28 recently → NOT hot (below own baseline)
{ name: 'Star', games: [{ points: 28 }, { points: 27 }, { points: 26 }, { points: 31 }, { points: 33 }, { points: 30 }, { points: 32 }, { points: 31 }, { points: 30 }, { points: 33 }] },
];
const list = computeHotList(players, 'nba', { stat: 'points', window: 3 });
expect(list.map((p) => p.name)).toContain('Riser');
expect(list.map((p) => p.name)).not.toContain('Star');
});
test('returns a ranked list with rank field', () => {
const players = [
{ name: 'A', games: [{ points: 40 }, { points: 40 }, { points: 10 }, { points: 10 }] },
{ name: 'B', games: [{ points: 22 }, { points: 22 }, { points: 18 }, { points: 18 }] },
];
const list = computeHotList(players, 'nba', { stat: 'points', window: 2 });
expect(list[0].rank).toBe(1);
expect(list[0].name).toBe('A'); // biggest jump above baseline
expect(list[0].delta).toBeGreaterThan(list[1].delta);
});
test('uses explicit seasonAvg as baseline when present', () => {
const players = [
{ name: 'C', seasonAvg: { points: 15 }, games: [{ points: 25 }, { points: 25 }] },
];
const list = computeHotList(players, 'nba', { stat: 'points', window: 2 });
expect(list[0].baseline).toBe(15);
expect(list[0].delta).toBe(10);
});
test('7-day window filters by date when dates + now supplied', () => {
const now = new Date('2026-06-12T12:00:00Z').getTime();
const day = 86_400_000;
const players = [
{ name: 'D', games: [
{ date: '2026-06-11', hits: 3 }, // in window
{ date: '2026-06-10', hits: 2 }, // in window
{ date: '2026-06-01', hits: 0 }, // outside 7d → baseline
{ date: '2026-05-30', hits: 0 },
] },
];
const list = computeHotList(players, 'mlb', { stat: 'hits', windowDays: 7, now });
expect(list).toHaveLength(1);
expect(list[0].window).toBe(2); // only the 2 recent games
expect(list[0].recentAvg).toBe(2.5);
});
test('player with no baseline to trend against is excluded', () => {
const players = [
{ name: 'E', games: [{ points: 30 }, { points: 30 }] }, // window 7 → no rest, no seasonAvg
];
const list = computeHotList(players, 'nba', { stat: 'points' });
expect(list).toHaveLength(0);
});
test('tie on delta broken by raw recent average', () => {
const players = [
{ name: 'Low', games: [{ points: 15 }, { points: 15 }, { points: 10 }, { points: 10 }] }, // +5, recent 15
{ name: 'High', games: [{ points: 25 }, { points: 25 }, { points: 20 }, { points: 20 }] }, // +5, recent 25
];
const list = computeHotList(players, 'nba', { stat: 'points', window: 2 });
expect(list[0].name).toBe('High');
});
test('empty input → empty list', () => {
expect(computeHotList([], 'nba')).toEqual([]);
expect(computeHotList(null, 'nba')).toEqual([]);
});
test('all/default stat resolves to the sport headline stat', () => {
const players = [{ name: 'F', games: [{ hits: 3 }, { hits: 3 }, { hits: 0 }, { hits: 0 }] }];
const list = computeHotList(players, 'mlb', { stat: 'all', window: 2 });
expect(list[0].stat).toBe('hits');
});
});