Files
vyndr/tests/unit/snapshotScheduler.test.js
T
builtbykev cdedecf55b Session 52: Coming Soon teaser + infrastructure verification (2239 tests)
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>
2026-06-19 13:55:39 -04:00

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));
});
});