/** * Session 64 — CLOSING-LINE CAPTURE (capture only; no CLV metric). * * Written BEFORE the capture logic. The first property proven is the refusal: * a prop we cannot legitimately price at the close must record `missed_close` * and NO price. Substituting a stale or mid-day line would manufacture a CLV * proof out of a number that was never the close — the exact failure this * whole substrate exists to prevent. */ const cap = require('../../src/services/closingCapture'); const prop = (o = {}) => ({ player: 'José Ramírez', stat_type: 'hits', line: 1.5, over_odds: -120, under_odds: 100, book: 'draftkings', game_time: '2026-07-21T23:10:00Z', home_team: 'CLE', away_team: 'MIN', ...o, }); const NOW = new Date('2026-07-21T22:55:00Z'); // 15 min before lock describe('THE REFUSAL — never fabricate a close', () => { test('a prop with NO bound game_time records missed_close, no price', () => { const rows = cap.buildCaptureRows('mlb', [prop({ game_time: null })], { now: NOW }); // BOTH sides are recorded as missed — a refusal is per-side, exactly like a // capture, so the join finds an explicit answer for either side. expect(rows).toHaveLength(2); expect(rows[0].missed_reason).toBe('unbound_game_time'); expect(rows[0].over_odds).toBeNull(); expect(rows[0].under_odds).toBeNull(); expect(rows[0].line).toBeNull(); }); test('a DOUBLEHEADER prop is ambiguous — recorded, never attributed', () => { const rows = cap.buildCaptureRows('mlb', [prop({ game_ambiguous: true })], { now: NOW }); expect(rows[0].missed_reason).toBe('doubleheader_ambiguous'); expect(rows[0].over_odds).toBeNull(); }); test('a game already LOCKED (past start) records missed_close, not a stale line', () => { const late = new Date('2026-07-21T23:30:00Z'); // 20 min AFTER first pitch const rows = cap.buildCaptureRows('mlb', [prop()], { now: late }); expect(rows[0].missed_reason).toBe('missed_window'); expect(rows[0].over_odds).toBeNull(); }); test('a prop far from lock is NOT captured at all (not yet the close)', () => { const early = new Date('2026-07-21T12:00:00Z'); // 11h before const rows = cap.buildCaptureRows('mlb', [prop()], { now: early }); expect(rows).toHaveLength(0); }); test('no capture row ever carries a price without a real close', () => { const rows = cap.buildCaptureRows('mlb', [ prop({ game_time: null }), prop({ game_ambiguous: true }), ], { now: NOW }); for (const r of rows) { expect(r.missed_reason).toBeTruthy(); expect(r.over_odds).toBeNull(); expect(r.under_odds).toBeNull(); } }); }); describe('CAPTURE — inside the pre-lock window', () => { test('captures BOTH side prices raw (de-vig is computed downstream, not here)', () => { const [r] = cap.buildCaptureRows('mlb', [prop()], { now: NOW }); expect(r.missed_reason).toBeNull(); expect(r.over_odds).toBe(-120); expect(r.under_odds).toBe(100); expect(r.line).toBe(1.5); }); test('a one-sided price is NOT a usable close — recorded as incomplete', () => { const [r] = cap.buildCaptureRows('mlb', [prop({ under_odds: null })], { now: NOW }); expect(r.missed_reason).toBe('one_sided_price'); expect(r.over_odds).toBeNull(); }); test('rows carry the natural JOIN KEY minus line (a close moves off the graded line)', () => { const [r] = cap.buildCaptureRows('mlb', [prop()], { now: NOW }); expect(r.sport).toBe('mlb'); expect(r.player_key).toBe('jose ramirez'); expect(r.stat).toBe('hits'); expect(r.game_date).toBe('2026-07-21'); expect(r).toHaveProperty('side'); }); test('both sides of a prop are captured as separate joinable rows', () => { const rows = cap.buildCaptureRows('mlb', [prop()], { now: NOW }); expect(rows.map((r) => r.side).sort()).toEqual(['over', 'under']); }); test('every row is timestamped and immutable-by-construction (append-only)', () => { const [r] = cap.buildCaptureRows('mlb', [prop()], { now: NOW }); expect(r.captured_at).toBe(NOW.toISOString()); expect(r.book).toBe('draftkings'); }); test('sharp vs book close are distinguished (different CLV questions)', () => { const rows = cap.buildCaptureRows('mlb', [prop({ book: 'pinnacle' })], { now: NOW }); expect(rows[0].line_type).toBe('sharp'); const rows2 = cap.buildCaptureRows('mlb', [prop({ book: 'draftkings' })], { now: NOW }); expect(rows2[0].line_type).toBe('book'); }); }); describe('capture-rate alarm (silent-failure discipline)', () => { test('alarms when most in-window props failed to capture', () => { const r = cap.captureRateAlarm({ eligible: 100, captured: 40, missed: 60 }); expect(r.alarm).toBe(true); }); test('quiet on a healthy pass', () => { expect(cap.captureRateAlarm({ eligible: 100, captured: 95, missed: 5 }).alarm).toBe(false); }); test('nothing eligible never alarms', () => { expect(cap.captureRateAlarm({ eligible: 0, captured: 0, missed: 0 }).alarm).toBe(false); }); }); describe('RETRY — bounded, lock-walled, never a post-lock line (Phase 1)', () => { const LOCK = '2026-07-21T23:10:00Z'; const at = (iso) => new Date(iso); test('a flaky fetch that SUCCEEDS inside the window captures normally', async () => { let calls = 0; const res = await cap.captureWithRetry('mlb', { now: () => at('2026-07-21T22:50:00Z'), nextLockAt: LOCK, attempts: 3, sleep: async () => {}, fetchProps: async () => { calls += 1; if (calls < 3) throw new Error('feed flaked'); return [prop()]; }, }); expect(calls).toBe(3); expect(res.gave_up).toBe(false); expect(res.rows.filter((r) => !r.missed_reason)).toHaveLength(2); }); test('attempts are BOUNDED — it gives up and records missed_close with NO price', async () => { let calls = 0; const res = await cap.captureWithRetry('mlb', { now: () => at('2026-07-21T22:50:00Z'), nextLockAt: LOCK, attempts: 3, sleep: async () => {}, fetchProps: async () => { calls += 1; throw new Error('feed down'); }, fallbackProps: [prop()], }); expect(calls).toBe(3); // bounded, not infinite expect(res.gave_up).toBe(true); expect(res.rows).toHaveLength(2); for (const r of res.rows) { expect(r.missed_reason).toBe('fetch_failed'); expect(r.over_odds).toBeNull(); expect(r.line).toBeNull(); } }); test('HARD LOCK-WALL: inside the wall it stops retrying and records missed_close', async () => { let calls = 0; const res = await cap.captureWithRetry('mlb', { now: () => at('2026-07-21T23:09:00Z'), // 1 min to lock, wall is 2 nextLockAt: LOCK, lockWallMinutes: 2, attempts: 5, sleep: async () => {}, fetchProps: async () => { calls += 1; return [prop()]; }, fallbackProps: [prop()], }); expect(calls).toBe(0); // never even tries expect(res.reason).toBe('lock_wall'); expect(res.rows.every((r) => r.missed_reason === 'missed_window')).toBe(true); expect(res.rows.every((r) => r.over_odds === null)).toBe(true); }); test('PAST lock never captures — a line at/after lock is not a close', async () => { const res = await cap.captureWithRetry('mlb', { now: () => at('2026-07-21T23:30:00Z'), nextLockAt: LOCK, attempts: 3, sleep: async () => {}, fetchProps: async () => [prop()], fallbackProps: [prop()], }); expect(res.reason).toBe('lock_wall'); expect(res.rows.every((r) => r.over_odds === null)).toBe(true); }); test('an UNBOUND prop records missed_close immediately and never enters the retry loop', async () => { let calls = 0; const res = await cap.captureWithRetry('mlb', { now: () => at('2026-07-21T22:50:00Z'), nextLockAt: null, // nothing to key the close to attempts: 5, sleep: async () => {}, fetchProps: async () => { calls += 1; return [prop({ game_time: null })]; }, fallbackProps: [prop({ game_time: null })], }); expect(calls).toBe(0); expect(res.reason).toBe('no_bound_lock'); expect(res.rows.every((r) => r.missed_reason === 'unbound_game_time')).toBe(true); }); });