'use strict'; /** * THE MLB TIMING CONTRACT. * * A prop carrying a usable `game_time` skipped `bindGame` — and `bindGame` is * where `game_date` was assigned. So the fast path left props TIMED and * DATELESS. Once PropLine began supplying `game_time`, every prop took that * branch. * * Proven on production 2026-08-28T07:00Z, attempt * acq_972544a1-563e-47ba-bde4-2f73ecf536bd: 12,015 props acquired, * alreadyHad 12,015, dates_requested 0, schedule games 0, unresolved 12,015 * (`no_schedule`), admitted 0, graded 0. * * The repair is not a new rule: `bindGame` already defines * `game_date: etDate(prop.game_time)` for exactly this case, and * `ledgerService.gameDateFor` already PREFERS the derived ET date over a * provider-supplied one. This applies the contract the fast path skipped. */ const fs = require('fs'); const path = require('path'); const gb = require('../../src/services/gameBinder'); const ROOT = path.resolve(__dirname, '..', '..'); const noSchedule = { getSchedule: async () => [] }; const prop = (o = {}) => ({ player: 'P', stat_type: 'hits', line: 0.5, book: 'draftkings', ...o }); describe('THE CONTRACT: valid game_time yields the baseball game_date', () => { test('valid game_time + MISSING game_date -> filled', async () => { const p = prop({ game_time: '2026-08-28T18:20:00Z' }); const r = await gb.attachGameTimes('mlb', [p], noSchedule); expect(p.game_date).toBe('2026-08-28'); expect(r.alreadyHad).toBe(1); expect(r.bound).toBe(0); // NOT rebound merely to derive a date }); test('the derived date is the repository helper, not a second conversion', () => { const src = fs.readFileSync(path.join(ROOT, 'src/services/gameBinder.js'), 'utf8'); const i = src.indexOf('const etFast ='); expect(i).toBeGreaterThan(-1); const block = src.slice(i, i + 260); expect(block).toMatch(/etDate\(p\.game_time\)/); // No hardcoded offset, no second Intl formatter in the fast path. expect(block).not.toMatch(/Intl\.DateTimeFormat|America\/New_York|[+-]0[45]:00|getTimezoneOffset/); }); test('game_time is NEVER modified by the repair', async () => { const p = prop({ game_time: '2026-08-28T18:20:00Z' }); await gb.attachGameTimes('mlb', [p], noSchedule); expect(p.game_time).toBe('2026-08-28T18:20:00Z'); }); }); describe('PRE-EXISTING game_date POLICY', () => { test('existing MATCHING game_date is preserved', async () => { const p = prop({ game_time: '2026-08-28T18:20:00Z', game_date: '2026-08-28' }); await gb.attachGameTimes('mlb', [p], noSchedule); expect(p.game_date).toBe('2026-08-28'); }); test('existing DIFFERING game_date is NOT silently rewritten in this tranche', async () => { // Deliberately scoped: a conflicting provider date is future-hardening // territory, not something this repair may quietly overwrite. const p = prop({ game_time: '2026-08-28T18:20:00Z', game_date: '2026-01-01' }); await gb.attachGameTimes('mlb', [p], noSchedule); expect(p.game_date).toBe('2026-01-01'); }); test('the fill is conditional on absence', () => { const src = fs.readFileSync(path.join(ROOT, 'src/services/gameBinder.js'), 'utf8'); expect(src).toMatch(/if \(!p\.game_date\) p\.game_date = etFast;/); }); }); describe('INVALID game_time INVENTS NOTHING', () => { test('unparseable game_time gets no date and falls through to the binder', async () => { const p = prop({ game_time: 'not-a-time' }); const r = await gb.attachGameTimes('mlb', [p], noSchedule); expect(p.game_date).toBeUndefined(); expect(r.alreadyHad).toBe(0); expect(r.unresolved).toBe(1); // existing fallback semantics preserved }); test('absent game_time gets no date', async () => { const p = prop({}); await gb.attachGameTimes('mlb', [p], noSchedule); expect(p.game_date).toBeUndefined(); }); }); describe('UTC DATE IS NOT THE BASEBALL DATE', () => { // The whole point: a late US game is the PREVIOUS ET day. for (const [gameTime, etDay, why] of [ ['2026-08-28T18:20:00Z', '2026-08-28', 'afternoon ET'], ['2026-08-28T23:05:00Z', '2026-08-28', 'evening ET'], ['2026-08-28T01:45:00Z', '2026-08-27', 'late west-coast, UTC has already rolled over'], ['2026-08-28T03:10:00Z', '2026-08-27', 'extra innings past UTC midnight'], ]) { test(`${gameTime} -> ${etDay} (${why})`, async () => { const p = prop({ game_time: gameTime }); await gb.attachGameTimes('mlb', [p], noSchedule); expect(p.game_date).toBe(etDay); if (etDay !== gameTime.slice(0, 10)) { expect(p.game_date).not.toBe(gameTime.slice(0, 10)); // UTC date would be wrong } }); } test('DST boundary is handled by the repository helper, not an offset', async () => { // 2026-11-01 is the US DST fallback. 01:30Z on Nov 1 is 21:30 ET Oct 31 // (EDT, -4); 06:30Z is 02:30 ET Nov 1 (EST, -5). Both must land correctly. const a = prop({ game_time: '2026-11-01T01:30:00Z' }); const b = prop({ game_time: '2026-11-01T06:30:00Z' }); await gb.attachGameTimes('mlb', [a, b], noSchedule); expect(a.game_date).toBe('2026-10-31'); expect(b.game_date).toBe('2026-11-01'); }); }); describe('DOUBLEHEADER — one date, still two events', () => { test('both halves derive the SAME game_date, and identity still separates them', async () => { const g1 = prop({ player: 'Nolan Arenado', game_time: '2026-08-28T17:10:00Z', home_team: 'Cincinnati Reds', away_team: 'St. Louis Cardinals' }); const g2 = prop({ player: 'Nolan Arenado', game_time: '2026-08-28T21:40:00Z', home_team: 'Cincinnati Reds', away_team: 'St. Louis Cardinals' }); await gb.attachGameTimes('mlb', [g1, g2], noSchedule); // Same baseball date — expected, and what makes the schedule fetch possible. expect(g1.game_date).toBe('2026-08-28'); expect(g2.game_date).toBe('2026-08-28'); // The UNMODIFIED resolver still tells the two games apart, by clock. const evid = require('../../src/services/event/eventIdentity'); const games = [ { gamePk: 824514, gameDate: '2026-08-28T17:10:00Z', gameNumber: 1, home: { team: 'Cincinnati Reds' }, away: { team: 'St. Louis Cardinals' } }, { gamePk: 824478, gameDate: '2026-08-28T21:40:00Z', gameNumber: 2, home: { team: 'Cincinnati Reds' }, away: { team: 'St. Louis Cardinals' } }, ]; const out = evid.attachEventIdentity('mlb', [g1, g2], games, null, false); expect(out.canonical).toBe(2); expect(g1.canonical_event_id).not.toBe(g2.canonical_event_id); expect(g1.canonical_event_id).toBe('mlb:gamepk:824514'); expect(g2.canonical_event_id).toBe('mlb:gamepk:824478'); }); }); test('a time matching NEITHER game within an hour still REFUSES', async () => { // Without this the doubleheader guard could be deleted and the suite would // still pass, because a well-matched fixture picks the right game anyway. const evid = require('../../src/services/event/eventIdentity'); const stray = prop({ player: 'Nolan Arenado', game_time: '2026-08-29T05:00:00Z', home_team: 'Cincinnati Reds', away_team: 'St. Louis Cardinals' }); const games = [ { gamePk: 824514, gameDate: '2026-08-28T17:10:00Z', gameNumber: 1, home: { team: 'Cincinnati Reds' }, away: { team: 'St. Louis Cardinals' } }, { gamePk: 824478, gameDate: '2026-08-28T21:40:00Z', gameNumber: 2, home: { team: 'Cincinnati Reds' }, away: { team: 'St. Louis Cardinals' } }, ]; const out = evid.attachEventIdentity('mlb', [stray], games, null, false); expect(out.canonical).toBe(0); expect(stray.canonical_event_id).toBeNull(); expect(out.reasons.ambiguous_time_too_far).toBe(1); }); describe('ROOT-CAUSE REGRESSION FIXTURE (PropLine shape)', () => { // Exactly the production shape: valid game_time, NO game_date. const fixture = () => [ prop({ player: 'A', game_time: '2026-08-28T18:20:00Z', home_team: 'Chicago Cubs', away_team: 'Cincinnati Reds' }), prop({ player: 'B', game_time: '2026-08-28T18:20:00Z', home_team: 'Chicago Cubs', away_team: 'Cincinnati Reds' }), ]; const datesFrom = (props) => [...new Set(props.map((p) => p && p.game_date).filter(Boolean))]; test('BEFORE the repair the fixture produced dates_requested = 0', () => { // The defect reproduced without the fix: the fast path assigned nothing. const props = fixture(); for (const p of props) { if (p.game_time) { /* old fast path: continue */ } } expect(datesFrom(props)).toHaveLength(0); }); test('AFTER the repair the schedule stage receives the baseball date', async () => { const props = fixture(); await gb.attachGameTimes('mlb', props, noSchedule); const dates = datesFrom(props); expect(dates).toEqual(['2026-08-28']); expect(dates.length).toBeGreaterThan(0); // dates_requested > 0 }); test('with matching schedule evidence the props can now RESOLVE', async () => { const props = fixture(); await gb.attachGameTimes('mlb', props, noSchedule); const evid = require('../../src/services/event/eventIdentity'); const games = [{ gamePk: 824638, gameDate: '2026-08-28T18:20:00Z', gameNumber: 1, home: { team: 'Chicago Cubs' }, away: { team: 'Cincinnati Reds' } }]; const out = evid.attachEventIdentity('mlb', props, games, null, false); expect(out.canonical).toBe(2); expect(out.unresolved).toBe(0); expect(out.reasons.no_schedule).toBeUndefined(); }); test('with NO schedule the props still fail closed as no_schedule', async () => { const props = fixture(); await gb.attachGameTimes('mlb', props, noSchedule); const evid = require('../../src/services/event/eventIdentity'); const out = evid.attachEventIdentity('mlb', props, [], null, false); expect(out.canonical).toBe(0); expect(out.reasons.no_schedule).toBe(2); }); }); describe('ADMISSION STILL FAILS CLOSED', () => { const { __internals } = require('../../src/services/gradeSlateService'); for (const status of ['UNRESOLVED', 'AMBIGUOUS', 'CONTRADICTED']) { test(`${status} is still REJECTED after the repair`, () => { const p = prop({ game_time: '2026-08-28T18:20:00Z', game_date: '2026-08-28', event_binding_status: status }); const gate = __internals.admitForGrading([p], 'mlb'); expect(gate.admitted).toHaveLength(0); expect(gate.rejected).toHaveLength(1); }); } test('RESOLVED without a canonical id is still REJECTED', () => { const p = prop({ game_time: '2026-08-28T18:20:00Z', event_binding_status: 'RESOLVED' }); expect(__internals.admitForGrading([p], 'mlb').admitted).toHaveLength(0); }); test('RESOLVED WITH a canonical id is admitted', () => { const p = prop({ game_time: '2026-08-28T18:20:00Z', event_binding_status: 'RESOLVED', canonical_event_id: 'mlb:gamepk:1' }); expect(__internals.admitForGrading([p], 'mlb').admitted).toHaveLength(1); }); }); describe('THE BOUND PATH IS UNTOUCHED', () => { test('a prop with NO game_time is bound from the schedule, not from now()', async () => { // The fast-path tests never reach the bound branch, so a defect there would // otherwise be invisible. const p = prop({ player: 'X', home_team: 'Chicago Cubs', away_team: 'Cincinnati Reds' }); // bindGame reads `gameTime || date` from the schedule entry. const schedule = async () => ([{ id: 'g1', home: 'Chicago Cubs', away: 'Cincinnati Reds', gameTime: '2026-08-28T18:20:00Z', date: '2026-08-28', }]); const r = await gb.attachGameTimes('mlb', [p], { getSchedule: schedule, gradedAt: '2026-08-28T12:00:00Z' }); if (r.bound === 1) { expect(p.game_time).toBe('2026-08-28T18:20:00Z'); expect(p.game_date).toBe('2026-08-28'); } else { // If the fixture shape does not bind, it must still invent nothing. expect(p.game_time).toBeUndefined(); expect(p.game_date).toBeUndefined(); } }); test('the bound path assigns the SCHEDULE time verbatim', () => { const src = fs.readFileSync(path.join(ROOT, 'src/services/gameBinder.js'), 'utf8'); expect(src).toMatch(/p\.game_time = b\.game_time;/); expect(src).not.toMatch(/p\.game_time = new Date\(\)/); }); }); describe('SCOPE: only timing metadata changed', () => { test('the repair does not rebind, rematch teams, or touch identity', () => { const src = fs.readFileSync(path.join(ROOT, 'src/services/gameBinder.js'), 'utf8'); const i = src.indexOf('const etFast ='); const block = src.slice(i, src.indexOf('const b = await bindGame', i)); expect(block).not.toMatch(/bindGame|gameMatchesTeams|canonical|getSchedule/); }); test('bindGame itself is unchanged — the derivation it defines is reused', () => { const src = fs.readFileSync(path.join(ROOT, 'src/services/gameBinder.js'), 'utf8'); const body = src.slice(src.indexOf('async function bindGame'), src.indexOf('async function attachGameTimes')); expect(body).toMatch(/game_date: etDate\(prop\.game_time\)/); }); });