// S6 (A1 board) — ●●○●● last-10 vs tonight's locked line (ROW-GRAMMAR §4). // Pure math over the rosterlogs blob; stat access reuses streaksService's // accessors (one source of truth for every field spelling). Real logs only. const fs = require('fs'); const path = require('path'); const { computeLast10Dots, indexRosterLogs, attachLast10Dots, __internals } = require('../../src/services/last10Dots'); describe('computeLast10Dots', () => { const games = [ { date: '2026-07-10', hits: 2, totalBases: 5 }, { date: '2026-07-09', hits: 0, totalBases: 0 }, { date: '2026-07-08', hits: 1, totalBases: 2 }, ]; test('hit = value ≥ line, boolean per game, newest first', () => { expect(computeLast10Dots(games, 'mlb', 'hits', 1.5)).toEqual([true, false, false]); expect(computeLast10Dots(games, 'mlb', 'total_bases', 1.5)).toEqual([true, false, true]); }); test('tolerates the alternate field spellings (streaksService accessors)', () => { const alt = [{ H: 3 }, { h: 0 }]; expect(computeLast10Dots(alt, 'mlb', 'hits', 0.5)).toEqual([true, false]); }); test('nba/wnba stats work too (points, pra)', () => { const log = [{ pts: 31, reb: 8, ast: 7 }, { pts: 12, reb: 2, ast: 1 }]; expect(computeLast10Dots(log, 'wnba', 'points', 20.5)).toEqual([true, false]); expect(computeLast10Dots(log, 'nba', 'pra', 40.5)).toEqual([true, false]); }); test('caps at 10 games', () => { const long = Array.from({ length: 14 }, (_, i) => ({ hits: i % 2 })); expect(computeLast10Dots(long, 'mlb', 'hits', 0.5)).toHaveLength(10); }); test('absent → nothing: unknown stat, no games, bad line all return null', () => { expect(computeLast10Dots(games, 'mlb', 'exit_velocity', 1.5)).toBeNull(); expect(computeLast10Dots([], 'mlb', 'hits', 1.5)).toBeNull(); expect(computeLast10Dots(games, 'mlb', 'hits', null)).toBeNull(); expect(computeLast10Dots(games, 'unknown_sport', 'hits', 1.5)).toBeNull(); }); }); describe('indexRosterLogs + attachLast10Dots', () => { const blob = [ { name: 'A.J. Ewing', team: 'NYY', games: [{ hits: 2 }, { hits: 0 }, { hits: 1 }] }, { name: 'No Games Guy', team: 'BOS', games: [] }, ]; test('indexes by nameKey (variants merge) and skips empty logs', () => { const idx = indexRosterLogs(blob); expect(idx['aj ewing']).toHaveLength(3); expect(Object.keys(idx)).toHaveLength(1); }); test('attaches last10_dots against the LOCKED line; untouched when absent', () => { const idx = indexRosterLogs(blob); const grades = [ { player: 'AJ Ewing', stat_type: 'hits', line: 2.5, gradedAt: { line: 0.5 }, grade: 'A' }, { player: 'Someone Else', stat_type: 'hits', line: 0.5, grade: 'B' }, { player: 'AJ Ewing', stat_type: 'exit_velocity', line: 90, grade: 'C' }, ]; const out = attachLast10Dots(grades, idx, 'mlb'); // locked line (0.5) is used, not the current line (2.5). expect(out[0].last10_dots).toEqual([true, false, true]); expect(out[1].last10_dots).toBeUndefined(); // no log → nothing expect(out[2].last10_dots).toBeUndefined(); // no accessor → nothing expect(out[1]).toBe(grades[1]); // pass-through untouched }); test('empty roster index is a no-op', () => { const grades = [{ player: 'AJ Ewing', stat_type: 'hits', line: 0.5 }]; expect(attachLast10Dots(grades, {}, 'mlb')).toBe(grades); }); }); describe('wiring locks', () => { test('the public snapshot route reads rosterlogs and attaches dots', () => { const route = fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'routes', 'snapshot.js'), 'utf8'); expect(route).toContain('rosterlogs:${sport}'); expect(route).toContain('attachLast10Dots'); }); test('the slate adapter threads last10_dots + history onto strip props', () => { const adapter = fs.readFileSync(path.join(__dirname, '..', '..', 'web', 'src', 'lib', 'slateAdapter.js'), 'utf8'); expect(adapter).toContain('rec.last10_dots'); expect(adapter).toContain('rec.history'); }); test('every accessor is a function (the streaksService contract holds)', () => { for (const [sport, map] of Object.entries(__internals.STAT_ACCESSORS)) { for (const [stat, fn] of Object.entries(map)) { expect({ sport, stat, isFn: typeof fn === 'function' }).toEqual({ sport, stat, isFn: true }); } } }); });