167996d99a
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
162 lines
6.0 KiB
JavaScript
162 lines
6.0 KiB
JavaScript
// Tank01 daily prefetch (Session 15) — orchestrator that pulls the
|
|
// Redis cache keys session 14's augmentor reads. Tests cover:
|
|
// - graceful skip when adapter has no API key
|
|
// - budget cap respected
|
|
// - dry-run suppresses adapter calls
|
|
// - final-status box scores get pulled, non-final skipped
|
|
// - empty slate doesn't crash
|
|
|
|
const mockNbaGames = jest.fn();
|
|
const mockNbaBox = jest.fn();
|
|
const mockNbaOdds = jest.fn();
|
|
const mockNbaHasKey = jest.fn(() => true);
|
|
jest.mock('../../src/services/adapters/tank01NbaAdapter', () => ({
|
|
getNBAGamesForDate: (...a) => mockNbaGames(...a),
|
|
getNBABoxScore: (...a) => mockNbaBox(...a),
|
|
getNBABettingOdds: (...a) => mockNbaOdds(...a),
|
|
hasApiKey: (...a) => mockNbaHasKey(...a),
|
|
}));
|
|
|
|
const mockMlbSlate = jest.fn();
|
|
const mockMlbBox = jest.fn();
|
|
const mockMlbBvp = jest.fn();
|
|
const mockMlbHasKey = jest.fn(() => true);
|
|
jest.mock('../../src/services/adapters/tank01MlbAdapter', () => ({
|
|
getMLBDailyScoreboard: (...a) => mockMlbSlate(...a),
|
|
getMLBBoxScore: (...a) => mockMlbBox(...a),
|
|
getMLBBatterVsPitcher: (...a) => mockMlbBvp(...a),
|
|
hasApiKey: (...a) => mockMlbHasKey(...a),
|
|
}));
|
|
|
|
const prefetch = require('../../scripts/tank01-prefetch');
|
|
|
|
beforeEach(() => {
|
|
mockNbaGames.mockReset();
|
|
mockNbaBox.mockReset();
|
|
mockNbaOdds.mockReset();
|
|
mockNbaHasKey.mockReset().mockReturnValue(true);
|
|
mockMlbSlate.mockReset();
|
|
mockMlbBox.mockReset();
|
|
mockMlbBvp.mockReset();
|
|
mockMlbHasKey.mockReset().mockReturnValue(true);
|
|
});
|
|
|
|
describe('parseArgs', () => {
|
|
test('defaults', () => {
|
|
const a = prefetch.__internals.parseArgs(['node', 'script']);
|
|
expect(a.maxRequests).toBe(prefetch.__internals.DEFAULT_BUDGET);
|
|
expect(a.dryRun).toBe(false);
|
|
expect(a.sports).toEqual(['nba', 'mlb']);
|
|
});
|
|
test('--max=N', () => {
|
|
expect(prefetch.__internals.parseArgs(['node', 's', '--max=25']).maxRequests).toBe(25);
|
|
});
|
|
test('--max ignores non-numeric / non-positive', () => {
|
|
expect(prefetch.__internals.parseArgs(['node', 's', '--max=0']).maxRequests).toBe(80);
|
|
expect(prefetch.__internals.parseArgs(['node', 's', '--max=foo']).maxRequests).toBe(80);
|
|
});
|
|
test('--dry-run', () => {
|
|
expect(prefetch.__internals.parseArgs(['node', 's', '--dry-run']).dryRun).toBe(true);
|
|
});
|
|
test('--sports filter', () => {
|
|
expect(prefetch.__internals.parseArgs(['node', 's', '--sports=mlb']).sports).toEqual(['mlb']);
|
|
});
|
|
});
|
|
|
|
describe('budget tracker', () => {
|
|
test('counts spend, refuses past cap', () => {
|
|
const b = prefetch.__internals.makeBudget(3);
|
|
expect(b.canSpend()).toBe(true);
|
|
b.spend(); b.spend(); b.spend();
|
|
expect(b.canSpend()).toBe(false);
|
|
expect(b.spent()).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe('main — NBA path', () => {
|
|
test('skips entirely when adapter has no API key', async () => {
|
|
mockNbaHasKey.mockReturnValueOnce(false);
|
|
mockMlbHasKey.mockReturnValueOnce(false);
|
|
const r = await prefetch.main(['node', 'script', '--sports=nba']);
|
|
expect(r.nba.skipped).toBe('no_key');
|
|
expect(mockNbaGames).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('pulls slate, then box score per FINAL game, plus odds once', async () => {
|
|
mockNbaGames.mockResolvedValueOnce([
|
|
{ gameId: 'G1', gameStatus: 'Final' },
|
|
{ gameId: 'G2', gameStatus: 'InProgress' }, // skip
|
|
{ gameId: 'G3', gameStatus: 'Final' },
|
|
]);
|
|
mockNbaBox.mockResolvedValue({});
|
|
mockNbaOdds.mockResolvedValueOnce({});
|
|
const r = await prefetch.main(['node', 'script', '--sports=nba']);
|
|
expect(r.nba.games).toBe(3);
|
|
expect(r.nba.boxscores).toBe(2); // only the Finals
|
|
expect(r.nba.odds).toBe(true);
|
|
expect(mockNbaBox).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
test('budget cap stops fetches mid-loop', async () => {
|
|
mockNbaGames.mockResolvedValueOnce(
|
|
Array.from({ length: 10 }, (_, i) => ({ gameId: `G${i}`, gameStatus: 'Final' })),
|
|
);
|
|
mockNbaBox.mockResolvedValue({});
|
|
mockNbaOdds.mockResolvedValueOnce({});
|
|
// Budget = 4: one for getNBAGamesForDate, leaves 3 box-score
|
|
// slots (no odds — budget exhausted first).
|
|
const r = await prefetch.main(['node', 'script', '--sports=nba', '--max=4']);
|
|
expect(mockNbaGames).toHaveBeenCalledTimes(1);
|
|
expect(mockNbaBox).toHaveBeenCalledTimes(3);
|
|
expect(mockNbaOdds).not.toHaveBeenCalled();
|
|
expect(r.requestsSpent).toBe(4);
|
|
});
|
|
|
|
test('dry-run skips ALL adapter calls', async () => {
|
|
const r = await prefetch.main(['node', 'script', '--dry-run']);
|
|
expect(mockNbaGames).not.toHaveBeenCalled();
|
|
expect(mockNbaBox).not.toHaveBeenCalled();
|
|
expect(mockNbaOdds).not.toHaveBeenCalled();
|
|
expect(r.nba.skipped).toBe('dry_run');
|
|
expect(r.mlb.skipped).toBe('dry_run');
|
|
});
|
|
|
|
test('empty slate is not an error', async () => {
|
|
mockNbaGames.mockResolvedValueOnce([]);
|
|
mockNbaOdds.mockResolvedValueOnce({});
|
|
const r = await prefetch.main(['node', 'script', '--sports=nba']);
|
|
expect(r.nba.games).toBe(0);
|
|
expect(r.nba.boxscores).toBe(0);
|
|
// Odds still pulled — it's a single daily call, not per-game.
|
|
expect(r.nba.odds).toBe(true);
|
|
});
|
|
|
|
test('null slate (adapter returned null) is not an error', async () => {
|
|
mockNbaGames.mockResolvedValueOnce(null);
|
|
const r = await prefetch.main(['node', 'script', '--sports=nba']);
|
|
expect(r.nba.games).toBe(0);
|
|
expect(r.nba.boxscores).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('main — MLB path', () => {
|
|
test('skips when adapter has no API key', async () => {
|
|
mockMlbHasKey.mockReturnValueOnce(false);
|
|
const r = await prefetch.main(['node', 'script', '--sports=mlb']);
|
|
expect(r.mlb.skipped).toBe('no_key');
|
|
});
|
|
|
|
test('pulls scoreboard + box scores for Finals/Completed', async () => {
|
|
mockMlbSlate.mockResolvedValueOnce([
|
|
{ gameId: 'M1', gameStatus: 'Final' },
|
|
{ gameId: 'M2', gameStatus: 'In Progress' }, // skip
|
|
{ gameId: 'M3', gameStatus: 'Completed' },
|
|
]);
|
|
mockMlbBox.mockResolvedValue({});
|
|
const r = await prefetch.main(['node', 'script', '--sports=mlb']);
|
|
expect(r.mlb.games).toBe(3);
|
|
expect(r.mlb.boxscores).toBe(2);
|
|
expect(r.mlb.bvp_skipped_reason).toMatch(/scoreboard/i);
|
|
});
|
|
});
|