Files
vyndr/tests/unit/sportCadence.test.js
T
builtbykev 4cd933d83e Job 1: per-sport snapshot cadence (config, not baseball's rhythm for all)
Every ACTIVE sport was graded at all five MLB slots (14/19/22/1/3 UTC). Sports
post lines on different clocks, so that inheritance was wasteful both ways:
WNBA props aren't posted at 14:00 UTC (10am ET) → that slot always graded 0
(the audit's "wnba:0"); soccer odds come from the 500/MONTH odds-api key, so
five slots/day is a third of the budget for 1-2 matches.

New src/config/sportCadence.js is the single source of truth (config-over-
constants). Mapped from reality + quota headroom (PropLine 9k/day abundant,
odds-api 500/mo scarce):
  mlb    14/19/22/1/3  intraday   (full grid — games+props all day)
  nba    14/19/22/1/3  intraday   (in-season fits; off-season self-skips empty)
  wnba   19/22/1       intraday   (afternoon→evening ET; drops the 14/3 waste)
  soccer 14/19         NO intraday (WC live; 2 lean odds-api reads, key-protected)

The scheduler still fires at HOURS_UTC and the missed-cron watchdog still
references MLB (which runs every grid hour) — each slot now grades only
sportsForHour(h), and only intradaySports() get the 20-min refresh. Every
sport's hours are kept a subset of the firing grid (a boot-time guard + a test
warn if that's ever violated). Retune a sport by editing one table row.

Adaptive, not constant: near-zero when a sport is quiet, protecting the scarce
odds-api quota from being drained by noon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 16:17:55 -04:00

50 lines
2.2 KiB
JavaScript

'use strict';
// Job 1 — per-sport snapshot cadence config. Locks the posting-rhythm map and
// the two quota-discipline invariants so a future edit can't silently
// reintroduce baseball's one-size-fits-all rhythm.
const cadence = require('../../src/config/sportCadence');
const { HOURS_UTC } = require('../../src/snapshotScheduler');
describe('sportCadence', () => {
test('every sport is a subset of the scheduler firing grid (else it never runs)', () => {
// The scheduler only fires at HOURS_UTC; a sport hour outside that set would
// be silently dropped. This invariant is the whole reason the design keeps
// the union inside the grid.
for (const orphan of cadence.ALL_HOURS.filter((h) => !HOURS_UTC.includes(h))) {
throw new Error(`hour ${orphan} is scheduled for some sport but not in HOURS_UTC=${HOURS_UTC}`);
}
expect(cadence.ALL_HOURS.every((h) => HOURS_UTC.includes(h))).toBe(true);
});
test('WNBA skips 14:00 UTC (props not posted) — the wasted "wnba:0" slot', () => {
expect(cadence.sportsForHour(14)).toContain('mlb');
expect(cadence.sportsForHour(14)).not.toContain('wnba');
// but WNBA DOES run at its real hours
expect(cadence.sportsForHour(22)).toContain('wnba');
expect(cadence.sportsForHour(1)).toContain('wnba');
});
test('soccer runs only its two lean odds-api slots, never the full grid', () => {
expect(cadence.hoursFor('soccer')).toEqual([14, 19]);
expect(cadence.sportsForHour(22)).not.toContain('soccer');
expect(cadence.sportsForHour(1)).not.toContain('soccer');
expect(cadence.sportsForHour(3)).not.toContain('soccer');
});
test('soccer is excluded from intraday (protects the 500/mo odds-api key)', () => {
expect(cadence.intradaySports()).not.toContain('soccer');
expect(cadence.intradaySports()).toEqual(expect.arrayContaining(['mlb', 'nba', 'wnba']));
});
test('sportsForHour respects a candidate subset and unknown hours yield none', () => {
expect(cadence.sportsForHour(19, ['wnba', 'soccer'])).toEqual(expect.arrayContaining(['wnba', 'soccer']));
expect(cadence.sportsForHour(7)).toEqual([]); // 7 UTC — nobody posts
});
test('mlb keeps the full baseball grid', () => {
expect(cadence.hoursFor('mlb')).toEqual([14, 19, 22, 1, 3]);
});
});