cdedecf55b
Phase 1 — Push-to-Book teaser (feature not live; teaser only): - StatStrip: "BOOK IT ⟶" per graded prop (hover: "Push-to-Book coming soon"). - GradeResultCard: "PUSH-TO-BOOK · COMING SOON" footer. Phase 2 — infrastructure verification: - snapshotScheduler logs armed AND disarmed state (incl SNAPSHOT_CRON) so container logs disambiguate off-vs-crashed. - NEW GET /api/internal/snapshot/status (internal-key gated): cron_armed, cron_hours_utc, last_snapshot per sport (gradeCount/deltaCount), redis_keys existence map, ticker_count. The post-deploy pipeline health probe. - Finding: Redis AOF/RDB persistence is a server-side (Coolify) config the app can't set/verify — documented. Phase 3 — delta pipeline (verified sound, no fix needed): - runSnapshot already rotates :latest->:previous and diffs locked lines; added opt-in SNAPSHOT_DEBUG=1 [deltas] log + a trace test asserting :previous is preserved verbatim and the delta math is correct. Backend 2234 -> 2239 tests (+5), 192 suites. Web build clean (exit 0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
49 lines
2.2 KiB
JavaScript
49 lines
2.2 KiB
JavaScript
// Session 45 — in-process snapshot scheduler (gated, fires once per slot).
|
|
|
|
const { startSnapshotScheduler, HOURS_UTC } = require('../../src/snapshotScheduler');
|
|
|
|
afterEach(() => { delete process.env.SNAPSHOT_CRON; });
|
|
|
|
describe('startSnapshotScheduler', () => {
|
|
it('no-ops unless SNAPSHOT_CRON=1', () => {
|
|
delete process.env.SNAPSHOT_CRON;
|
|
expect(startSnapshotScheduler()).toBeNull();
|
|
});
|
|
|
|
it('defaults to the sports-cycle UTC hours', () => {
|
|
expect(HOURS_UTC).toEqual([14, 19, 22, 1, 3]);
|
|
});
|
|
|
|
it('logs an armed startup message including SNAPSHOT_CRON when started', () => {
|
|
process.env.SNAPSHOT_CRON = '1';
|
|
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
const sched = startSnapshotScheduler({ runAllSnapshots: jest.fn(), now: () => new Date(Date.UTC(2026, 5, 18, 12, 30, 0)) });
|
|
expect(spy).toHaveBeenCalledWith(expect.stringContaining('[snapshotScheduler] armed'));
|
|
expect(spy).toHaveBeenCalledWith(expect.stringContaining('SNAPSHOT_CRON=1'));
|
|
spy.mockRestore();
|
|
if (sched && sched.interval && sched.interval.unref) clearInterval(sched.interval);
|
|
});
|
|
|
|
it('fires runAllSnapshots at a scheduled hour, once per slot', async () => {
|
|
process.env.SNAPSHOT_CRON = '1';
|
|
const runAllSnapshots = jest.fn(async () => [{ sport: 'mlb', status: 'ok' }]);
|
|
let d = new Date(Date.UTC(2026, 5, 18, 14, 0, 0)); // 14:00 UTC — scheduled
|
|
const sched = startSnapshotScheduler({ runAllSnapshots, now: () => d });
|
|
await sched.tick();
|
|
await sched.tick(); // same slot — must NOT double-fire
|
|
expect(runAllSnapshots).toHaveBeenCalledTimes(1);
|
|
if (sched.interval && sched.interval.unref) clearInterval(sched.interval);
|
|
});
|
|
|
|
it('does not fire off-schedule (wrong hour / non-zero minute)', async () => {
|
|
process.env.SNAPSHOT_CRON = '1';
|
|
const runAllSnapshots = jest.fn();
|
|
const sched = startSnapshotScheduler({ runAllSnapshots, now: () => new Date(Date.UTC(2026, 5, 18, 15, 0, 0)) });
|
|
await sched.tick();
|
|
const sched2 = startSnapshotScheduler({ runAllSnapshots, now: () => new Date(Date.UTC(2026, 5, 18, 14, 30, 0)) });
|
|
await sched2.tick();
|
|
expect(runAllSnapshots).not.toHaveBeenCalled();
|
|
[sched, sched2].forEach((s) => s.interval && clearInterval(s.interval));
|
|
});
|
|
});
|