7712f0a442
The "SIGNAL LIVE vs STALE 8h" contradiction was a field mismatch, not a dead pipeline. updated_at is the grade-LOCK time (advances only on a full snapshot, 5×/day — grades never change in-game, so it is intentionally stable). The SYNC badge measured the 20-min intraday cadence (expected_interval_s=1200) against that 5×/day field → structurally guaranteed STALE between slots even when intraday refreshes lines perfectly. - snapshotService: full snapshot now seeds refreshed_at at lock time - intradayRefresh already bumps refreshed_at every ~20 min (unchanged) - /api/snapshot/summary + GET /:sport now expose refreshed_at (was written to Redis but never serialized → no public liveness signal existed) - LiveLayer SYNC badge measures freshness from refreshed_at (fallback updated_at) Exposing refreshed_at also gives a public heartbeat probe: it advances every intraday slot, so pipeline liveness is verifiable without container logs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
4.0 KiB
JavaScript
91 lines
4.0 KiB
JavaScript
// Session 57 (Phase 0) — GET /api/snapshot/summary: the HeartbeatBar's honest
|
||
// data source. Real graded counts + latest pipeline run time from cache-only
|
||
// Redis reads; must never invent numbers on a cold cache.
|
||
|
||
const express = require('express');
|
||
const request = require('supertest');
|
||
|
||
const store = {};
|
||
jest.mock('../../src/utils/redis', () => ({
|
||
cacheGet: jest.fn(async (k) => (k in store ? store[k] : null)),
|
||
cacheSet: jest.fn(async (k, v) => { store[k] = v; return true; }),
|
||
}));
|
||
|
||
function mountApp() {
|
||
delete require.cache[require.resolve('../../src/routes/snapshot')];
|
||
const snapshotRoutes = require('../../src/routes/snapshot');
|
||
const app = express();
|
||
app.use('/api/snapshot', snapshotRoutes);
|
||
return app;
|
||
}
|
||
|
||
beforeEach(() => {
|
||
for (const k of Object.keys(store)) delete store[k];
|
||
jest.clearAllMocks();
|
||
});
|
||
|
||
describe('GET /api/snapshot/summary', () => {
|
||
test('sums real graded counts across sports and reports the latest run', async () => {
|
||
store['snapshot:mlb:latest'] = {
|
||
updated_at: '2026-07-10T18:00:00.000Z',
|
||
grades: [{ player: 'A' }, { player: 'B' }, { player: 'C' }],
|
||
};
|
||
store['snapshot:wnba:latest'] = {
|
||
updated_at: '2026-07-10T19:00:00.000Z',
|
||
grades: [{ player: 'D' }],
|
||
};
|
||
const res = await request(mountApp()).get('/api/snapshot/summary');
|
||
expect(res.status).toBe(200);
|
||
expect(res.body.graded).toBe(4);
|
||
expect(res.body.updated_at).toBe('2026-07-10T19:00:00.000Z'); // latest run
|
||
expect(res.body.sports.mlb).toBe(3);
|
||
expect(res.body.sports.wnba).toBe(1);
|
||
expect(res.body.sports.nba).toBe(0); // off-season → honest zero
|
||
});
|
||
|
||
test('refreshed_at is the freshness heartbeat, distinct from the grade-lock updated_at', async () => {
|
||
// updated_at = grade LOCK (5×/day, stable). refreshed_at = last intraday
|
||
// line refresh (~20 min). The SYNC badge measures against refreshed_at —
|
||
// measuring the 20-min cadence against the lock field caused "STALE 8h".
|
||
store['snapshot:mlb:latest'] = {
|
||
updated_at: '2026-07-10T18:00:00.000Z', // graded at 2pm ET
|
||
refreshed_at: '2026-07-10T21:40:00.000Z', // lines refreshed at 5:40pm ET
|
||
grades: [{ player: 'A' }],
|
||
};
|
||
const res = await request(mountApp()).get('/api/snapshot/summary');
|
||
expect(res.body.updated_at).toBe('2026-07-10T18:00:00.000Z');
|
||
expect(res.body.refreshed_at).toBe('2026-07-10T21:40:00.000Z');
|
||
});
|
||
|
||
test('refreshed_at falls back to updated_at when a snapshot predates the field', async () => {
|
||
// Old snapshots written before this fix have no refreshed_at — the freshness
|
||
// signal must still resolve (never null while a grade lock exists).
|
||
store['snapshot:mlb:latest'] = { updated_at: '2026-07-10T18:00:00.000Z', grades: [{ player: 'A' }] };
|
||
const res = await request(mountApp()).get('/api/snapshot/summary');
|
||
expect(res.body.refreshed_at).toBe('2026-07-10T18:00:00.000Z');
|
||
});
|
||
|
||
test('falls back to the grades:{sport} envelope when no snapshot exists', async () => {
|
||
store['grades:mlb'] = { updated_at: '2026-07-10T16:00:00.000Z', grades: [{ player: 'A' }, { player: 'B' }] };
|
||
const res = await request(mountApp()).get('/api/snapshot/summary');
|
||
expect(res.body.graded).toBe(2);
|
||
expect(res.body.updated_at).toBe('2026-07-10T16:00:00.000Z');
|
||
});
|
||
|
||
test('cold cache → zeros and null timestamp, never invented numbers', async () => {
|
||
const res = await request(mountApp()).get('/api/snapshot/summary');
|
||
expect(res.status).toBe(200);
|
||
expect(res.body.graded).toBe(0);
|
||
expect(res.body.updated_at).toBeNull();
|
||
});
|
||
|
||
test('is not captured by the /:sport route (summary ≠ a sport)', async () => {
|
||
store['snapshot:mlb:latest'] = { updated_at: 'x', grades: [{ player: 'A' }] };
|
||
const res = await request(mountApp()).get('/api/snapshot/summary');
|
||
// The /:sport handler would return { sport, grades, deltas }.
|
||
expect(res.body.sport).toBeUndefined();
|
||
expect(res.body.grades).toBeUndefined();
|
||
expect(typeof res.body.graded).toBe('number');
|
||
});
|
||
});
|