2d413cfe1e
Diagnosis (why 500/500 went unpaged): the only regular odds-api burner was
futuresService, which called axios DIRECTLY — bypassing the gateway, so it
never hit recordCall (the ONE place the WARN/BLOCK pager fires) and never
respected the 95% block. It only syncFromHeaders, which updated the counter's
number SILENTLY. oddsService (which does go through the gateway) only touches
odds-api when PropLine fails, so recordCall for odds-api effectively never ran.
Result: the counter could reach 100% with neither pager firing.
Fixes (a silent drain is now impossible, not just guarded):
- futuresService routes through gateway.fetch('odds-api', …) → counted, blocked
at 95%, and reserve-gated. Closes the raw-axios bypass.
- Reserve floor in the gateway: a DISCRETIONARY call (futures/soccer) passes
reserve=ODDS_API_RESERVE (default 50) and is refused while remaining <= reserve.
The ESSENTIAL MLB prop-backup passes no reserve and may spend to the 95% block.
→ a futures/soccer drain can NEVER starve MLB's backup path.
- quotaTracker.syncFromHeaders (the AUTHORITATIVE number) now fires the same
once-per-period WARN/BLOCK alert on a crossing — extracted fireThresholdAlert
shared with recordCall. The header-only drain now pages.
- POST /api/internal/quota/test-alert (internal-key) test-fires the pager
end-to-end so ntfy delivery is verifiable on demand.
Also (reality-corrected cadence): WNBA restored to the full grid. 2026-07-15
had two AFTERNOON WNBA games finished before the 22 UTC slot — 14 UTC (10am ET)
is the only slot early enough for a 1pm ET game's props, and on PropLine the
extra slots cost a rounding error. Soccer stays the only trimmed sport (the
real odds-api discipline). Assumption corrected by observed data.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
52 lines
2.5 KiB
JavaScript
52 lines
2.5 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 keeps the full grid — PropLine-cheap; catches afternoon + late west-coast games', () => {
|
|
// Corrected from reality (2026-07-15: afternoon WNBA games finished before
|
|
// the 22 UTC slot). 14 UTC (10am ET) is the only slot early enough for a
|
|
// 1pm ET game's props; on PropLine the extra slots cost a rounding error.
|
|
expect(cadence.hoursFor('wnba')).toEqual([14, 19, 22, 1, 3]);
|
|
expect(cadence.sportsForHour(14)).toContain('wnba');
|
|
});
|
|
|
|
test('soccer is the ONLY restricted sport — two lean odds-api slots, never the full grid', () => {
|
|
// The genuine cadence win is protecting the 500/mo odds-api key. PropLine
|
|
// sports run the full grid cheaply; soccer alone is trimmed.
|
|
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]);
|
|
});
|
|
});
|