Files
builtbykev b5d3fd14bb S11 (a1): live tracking — the read locked, the game watched
MLB statsapi + WNBA ESPN live boxscores -> per-player current values
(live:{sport}:{date} TTL 90s, /api/live/:sport + Next proxy). Pure
propState math (HIT / ON PACE / NEEDS N / HOLDS / LINE PASSED — never
red in-progress), attachLiveProgress strip join on nameKey+statType,
proximity-to-hit slate float, StatStrip LiveTracker in the ROW-GRAMMAR
outcome slot (spec amended + lock test updated). Grades never change
in-game — tracking, labeled as such. 2698 -> 2757 tests, web build 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 00:45:29 -04:00

70 lines
2.7 KiB
JavaScript

'use strict';
/**
* GET /api/live/:sport (A1 Session 11) — route wiring over the cache-aside
* service. The service's cache behavior is unit-tested with injected deps
* (liveTrackingService.test.js); here we lock the route contract: sport
* gating BEFORE the service, envelope pass-through, and fail-open 200s.
*/
const express = require('express');
const request = require('supertest');
jest.mock('../../src/services/liveTrackingService', () => ({
getLiveTracking: jest.fn(),
}));
const { getLiveTracking } = require('../../src/services/liveTrackingService');
function mountLive() {
delete require.cache[require.resolve('../../src/routes/live')];
const liveRoutes = require('../../src/routes/live');
const app = express();
app.use('/api/live', liveRoutes);
return app;
}
beforeEach(() => jest.clearAllMocks());
describe('GET /api/live/:sport', () => {
it('returns the service envelope for a wired sport (mlb)', async () => {
const envelope = {
sport: 'mlb', date: '2026-07-11', hasLive: true, updated_at: 'x',
games: [{ id: '824249', progress: { label: '▼8th', fraction: 0.83 }, players: { 'bryce harper': { name: 'Bryce Harper', values: { total_bases: 3 } } } }],
};
getLiveTracking.mockResolvedValue(envelope);
const res = await request(mountLive()).get('/api/live/mlb');
expect(res.status).toBe(200);
expect(res.body).toEqual(envelope);
expect(getLiveTracking).toHaveBeenCalledWith('mlb');
expect(res.headers['cache-control']).toContain('max-age=30');
});
it('wnba is wired', async () => {
getLiveTracking.mockResolvedValue({ sport: 'wnba', hasLive: false, games: [] });
const res = await request(mountLive()).get('/api/live/wnba');
expect(res.status).toBe(200);
expect(getLiveTracking).toHaveBeenCalledWith('wnba');
});
it('an unwired sport short-circuits WITHOUT touching the service (no quota risk)', async () => {
const res = await request(mountLive()).get('/api/live/nba');
expect(res.status).toBe(200);
expect(res.body).toEqual({ sport: 'nba', hasLive: false, games: [] });
expect(getLiveTracking).not.toHaveBeenCalled();
});
it('a service failure fails OPEN — 200 with an honest empty envelope', async () => {
getLiveTracking.mockRejectedValue(new Error('redis down'));
const res = await request(mountLive()).get('/api/live/mlb');
expect(res.status).toBe(200);
expect(res.body).toEqual({ sport: 'mlb', hasLive: false, games: [] });
});
it('sport param is case-insensitive', async () => {
getLiveTracking.mockResolvedValue({ sport: 'mlb', hasLive: false, games: [] });
const res = await request(mountLive()).get('/api/live/MLB');
expect(res.status).toBe(200);
expect(getLiveTracking).toHaveBeenCalledWith('mlb');
});
});