Files
vyndr/tests/unit/gameBinder.test.js
T
builtbykev 1bcdd8b305 Fix the ROOT: bind props to their real game, never date by the grade clock
Order 1.5 Phase 1. PropLine emits NO commence_time (grep-verified: zero
hits in proplineAdapter), so ledgerService's
`dateET(prop.game_time) || dateET(gradedTs)` always fell through to the
GRADE timestamp — and a 01:00/03:00 UTC snapshot is 21:00/23:00 ET the
PREVIOUS day. Tonight's props were filed under yesterday, settlement
correctly found no game there, and Order 1's void logic turned that into
64 destroyed results.

gameBinder.attachGameTimes() now matches every prop to a scheduled game by
TEAMS across the plausible ET window (grade date, +1, -1) and attaches the
GAME'S OWN time/date/id. It runs in snapshotService before grading and
before the ledger write, so ledger, retention and settlement all inherit
the correct date from one place.

HARD CONTRACT: an unbindable prop returns NOTHING. ledgerService no longer
has a grade-clock fallback — a row with no real game time is SKIPPED and
counted, because a mis-dated row is fabricated data and the ledger holds
real values or nothing. A slate that binds nothing pages.

DOUBLEHEADERS are reported, never guessed: two games with the same teams
on one date mark the binding `ambiguous` so settlement can decline rather
than attribute a prop to the wrong game. (Real example already in the
data: mlb:2026-07-11:MilwaukeeBrewers@PittsburghPirates(Game1).)

Also fixes retention, which had the SAME bug from last night — I had
dated model_snapshots rows with the snapshot clock. Rows now take the ET
date of the bound game_time.

Suite 282/3381 green, build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
2026-07-20 03:25:01 -04:00

89 lines
3.7 KiB
JavaScript

/**
* Session 64 Order 1.5 — bind a prop to its REAL game.
*
* The root bug: PropLine emits no commence_time, so ledgerService fell back to
* dating rows by the GRADE timestamp. A 01:00/03:00 UTC snapshot is the
* previous ET day, so tonight's props were filed under yesterday — which made
* settlement impossible and then got 64 rows wrongly voided as DNP.
*/
const binder = require('../../src/services/gameBinder');
const GAME = (id, home, away, time) => ({ id, homeTeam: { abbreviation: home }, awayTeam: { abbreviation: away }, gameTime: time });
describe('candidateDates', () => {
test('a 03:00 UTC grade (23:00 ET previous day) considers the NEXT ET day', () => {
// 2026-07-19T03:00Z = 2026-07-18 23:00 ET
const c = binder.candidateDates('2026-07-19T03:00:00Z');
expect(c[0]).toBe('2026-07-18');
expect(c).toContain('2026-07-19'); // the real game day
});
test('also looks one day back for an in-progress game', () => {
expect(binder.candidateDates('2026-07-19T18:00:00Z')).toContain('2026-07-18');
});
});
describe('bindGame', () => {
const prop = { player: 'X', stat_type: 'hits', home_team: 'CLE', away_team: 'PIT' };
test('trusts a real game_time when the feed provides one', async () => {
const r = await binder.bindGame({
sport: 'mlb', prop: { ...prop, game_time: '2026-07-19T23:10:00Z' },
gradedAt: '2026-07-19T03:00:00Z', getSchedule: async () => [],
});
expect(r.game_date).toBe('2026-07-19');
});
test('binds by TEAMS to the real game when game_time is missing', async () => {
const r = await binder.bindGame({
sport: 'mlb', prop, gradedAt: '2026-07-19T03:00:00Z',
getSchedule: async (sp, d) => (d === '2026-07-19'
? [GAME('401', 'CLE', 'PIT', '2026-07-19T23:10:00Z')] : []),
});
expect(r.game_date).toBe('2026-07-19'); // NOT the 07-18 grade date
expect(r.game_id).toBe('mlb:2026-07-19:401');
});
test('unbindable prop returns NULL — never a guessed date', async () => {
const r = await binder.bindGame({
sport: 'mlb', prop, gradedAt: '2026-07-19T03:00:00Z', getSchedule: async () => [],
});
expect(r).toBeNull();
});
test('DOUBLEHEADER is flagged ambiguous, not silently picked', async () => {
const r = await binder.bindGame({
sport: 'mlb', prop, gradedAt: '2026-07-19T03:00:00Z',
getSchedule: async (sp, d) => (d === '2026-07-19' ? [
GAME('401', 'CLE', 'PIT', '2026-07-19T17:10:00Z'),
GAME('402', 'CLE', 'PIT', '2026-07-19T23:10:00Z'),
] : []),
});
expect(r.ambiguous).toBe(true);
});
test('team orientation is tolerated (feeds disagree on home/away)', async () => {
const r = await binder.bindGame({
sport: 'mlb', prop, gradedAt: '2026-07-19T03:00:00Z',
getSchedule: async (sp, d) => (d === '2026-07-19' ? [GAME('401', 'PIT', 'CLE', '2026-07-19T23:10:00Z')] : []),
});
expect(r).not.toBeNull();
});
});
describe('attachGameTimes', () => {
test('binds a slate and reports counts (a silent collapse must be visible)', async () => {
const props = [
{ player: 'A', home_team: 'CLE', away_team: 'PIT' },
{ player: 'B', home_team: 'ZZZ', away_team: 'YYY' }, // unbindable
{ player: 'C', home_team: 'CLE', away_team: 'PIT', game_time: '2026-07-19T23:10:00Z' },
];
const out = await binder.attachGameTimes('mlb', props, {
gradedAt: '2026-07-19T03:00:00Z',
getSchedule: async (sp, d) => (d === '2026-07-19' ? [GAME('401', 'CLE', 'PIT', '2026-07-19T23:10:00Z')] : []),
});
expect(out).toMatchObject({ total: 3, bound: 1, alreadyHad: 1, unresolved: 1 });
expect(props[0].game_time).toBe('2026-07-19T23:10:00Z');
expect(props[1].game_time).toBeUndefined(); // left unresolved, not invented
});
});