/* ============================================================ Session 79 — priced-line surfacer + no-market empty state. NON-FABRICATING: only real snapshot lines with a real book price surface. ============================================================ */ const { indexPricedLines, pricedLinesFor } = require('../../web/src/lib/pricedLines'); const GRADES = [ { player_name: 'Gabriel Arias', stat_type: 'hits', line: 0.5, direction: 'over', book_odds: -165, fair_odds: -140 }, { player_name: 'Ryan Jeffers', stat_type: 'hits', line: 1.5, direction: 'under', book_odds: -160, fair_odds: -135 }, { player_name: 'Chase DeLauter', stat_type: 'doubles', line: 0.5, direction: 'over', book_odds: 340, fair_odds: 370 }, // no market → must NOT surface { player_name: 'No Market Guy', stat_type: 'hits', line: 0.5, direction: 'over', book_odds: null }, ]; describe('indexPricedLines — only real, priced rows', () => { it('indexes a priced row by player+stat', () => { const idx = indexPricedLines(GRADES); const rows = pricedLinesFor(idx, 'Gabriel Arias', 'hits'); expect(rows).toEqual([{ line: 0.5, direction: 'over', book_odds: -165 }]); }); it('EXCLUDES a graded row with no book price — not a surfaceable line', () => { const idx = indexPricedLines(GRADES); expect(pricedLinesFor(idx, 'No Market Guy', 'hits')).toEqual([]); }); it('a DIFFERENT stat priced for the same player returns nothing for the picked stat', () => { const idx = indexPricedLines(GRADES); // DeLauter has doubles priced, NOT hits. expect(pricedLinesFor(idx, 'Chase DeLauter', 'hits')).toEqual([]); expect(pricedLinesFor(idx, 'Chase DeLauter', 'doubles')).toHaveLength(1); }); it('an off-slate player returns nothing — never a nearest/suggested line', () => { const idx = indexPricedLines(GRADES); expect(pricedLinesFor(idx, 'Aaron Judge', 'hits')).toEqual([]); }); it('resolves name variants via nameKey (A.J. / AJ)', () => { const idx = indexPricedLines([{ player_name: 'A.J. Ewing', stat_type: 'hits', line: 0.5, direction: 'over', book_odds: -110 }]); expect(pricedLinesFor(idx, 'AJ Ewing', 'hits')).toHaveLength(1); }); it('sorts multiple priced lines by line', () => { const idx = indexPricedLines([ { player_name: 'X', stat_type: 'hits', line: 1.5, direction: 'over', book_odds: 120 }, { player_name: 'X', stat_type: 'hits', line: 0.5, direction: 'over', book_odds: -150 }, ]); expect(pricedLinesFor(idx, 'X', 'hits').map((r) => r.line)).toEqual([0.5, 1.5]); }); it('never fabricates on empty/garbage input', () => { expect(pricedLinesFor(indexPricedLines([]), 'X', 'hits')).toEqual([]); expect(pricedLinesFor(indexPricedLines(null), 'X', 'hits')).toEqual([]); expect(pricedLinesFor(null, 'X', 'hits')).toEqual([]); }); it('dedupes a repeated line+side', () => { const idx = indexPricedLines([ { player_name: 'X', stat_type: 'hits', line: 0.5, direction: 'over', book_odds: -150 }, { player_name: 'X', stat_type: 'hits', line: 0.5, direction: 'over', book_odds: -150 }, ]); expect(pricedLinesFor(idx, 'X', 'hits')).toHaveLength(1); }); }); describe('the empty state + surfacer are independently reversible', () => { const fs = require('fs'); it('the working card/join/triplet are untouched (Scan A byte-identical)', () => { // These files must NOT reference the new empty-state — proving the new UI is // additive and removable without touching the working path. const card = fs.readFileSync(require.resolve('../../web/src/components/vyndr/GradeResultCard.tsx'), 'utf8'); const route = fs.readFileSync(require.resolve('../../web/src/app/api/scan/route.ts'), 'utf8'); expect(card).not.toMatch(/NoMarketState/); expect(route).not.toMatch(/NoMarketState|pricedLines/); }); it('the empty state lives entirely in the scan page + its own component', () => { const page = fs.readFileSync(require.resolve('../../web/src/app/scan/page.tsx'), 'utf8'); expect(page).toMatch(/NoMarketState/); expect(page).toMatch(/result\.book_odds == null \|\| result\.fair_odds == null/); }); });