Files
vyndr/tests/unit/pricedLines.test.js
T
builtbykev 125919f86a Scanner reskin: amber → blue boundary channel + build never-built S6/S7 states
Semantic COLOR fix, not cosmetic. The S6/S7 build predated the current Scanner
States spec: it used AMBER (the quarantine / model-suppressed channel) for the
"no market / line not priced" case, telling users "model suppressed" when the
truth is "the board never priced this." Corrected to the HANDOFF Session-3
blue-boundary law: BLUE (--priced-out #8FB2DE) = no-market boundary; amber stays
QUARANTINE; red stays REFUSAL.

Phase 1 (reskin): NoMarketState → dashed BLUE void box + blue header/copy; the
S7 rows → spec format (o 27.5 · BK −114 · ◆ −105 · OPEN READ ▸) at 44px,
390-legible. Input-area surfacer pills reskinned to the blue channel too.

Phase 2 (never-built states, only those Phase 0 confirmed against live data):
- GREEN CTA with LIVE player count ("PLAYER · N PRICED PROPS ▸"), degrading
  honestly to the board path ("N PROPS LIVE · TONIGHT'S BOARD ▸") at 0 — count
  from the SAME fresh index as the rows (pricedCountForPlayer), can't disagree.
- CASE A none-priced DEFAULT: "WE PRICE THESE FOR [player]" — the player's other
  priced stats (pricedStatsForPlayer, filter by nameKey).
- Typed-line-mismatch blue fact line ("o X ISN'T PRICED · NEAREST ↓").

FLAGGED / not built (no shells): Case C off-slate quiet-stop needs schedule/
roster membership the pricedLines index doesn't carry (out of the presentation
fence). Spec CONTRADICTION: Case B says "fair previews amber," but the law
reserves amber for quarantine — fair renders NEUTRAL ◆ (blue-dim), not amber, to
avoid blurring the channel.

Free-tier gate VERIFIED before rendering FAIR: fair_odds is the de-vigged MARKET
price (valueState: "never hide the honest fair number"), NOT the gated
model_odds — no paid leak. Carried through indexPricedLines (additive; keying/
refresh/onPick/stale-tap all unchanged — the proven S7 data path is untouched).
Scan A byte-identical; PRICED_NUDGE_ENABLED still the kill switch; reversible.
Build exit 0; priced + parity suites green (67).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
2026-07-22 21:52:41 -04:00

176 lines
8.5 KiB
JavaScript

/* ============================================================
Session 79 — priced-line surfacer + no-market empty state.
NON-FABRICATING: only real snapshot lines with a real book price surface.
============================================================ */
const {
indexPricedLines, pricedLinesFor, pricedStatsForPlayer, pricedCountForPlayer,
totalPricedCount, nearestPricedLine,
} = 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');
// fair_odds is the de-vigged MARKET price, carried through for the S7 ◆ FAIR
// cell (free-tier visible — NOT the gated model_odds).
expect(rows).toEqual([{ line: 0.5, direction: 'over', book_odds: -165, fair_odds: -140 }]);
});
it('carries fair_odds strictly (null when absent — never Number(null)===0)', () => {
const idx = indexPricedLines([
{ player_name: 'Y', stat_type: 'hits', line: 0.5, direction: 'over', book_odds: -110 }, // no fair
]);
expect(pricedLinesFor(idx, 'Y', 'hits')[0].fair_odds).toBeNull();
});
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);
});
});
// ── S7 reskin — count + Case-A helpers, all from the SAME fresh index ────────
describe('pricedStatsForPlayer / count / board — one source, honest degrade', () => {
const MULTI = [
{ player_name: 'Justin Herbert', stat_type: 'pass_yds', line: 275.5, direction: 'over', book_odds: -115, fair_odds: -108 },
{ player_name: 'Justin Herbert', stat_type: 'pass_td', line: 1.5, direction: 'over', book_odds: 120, fair_odds: 135 },
{ player_name: 'Justin Herbert', stat_type: 'interceptions', line: 0.5, direction: 'over', book_odds: 150, fair_odds: 170 },
{ player_name: 'Other Guy', stat_type: 'hits', line: 0.5, direction: 'over', book_odds: -110, fair_odds: -100 },
];
it('pricedStatsForPlayer returns the player\'s priced stats across the board', () => {
const idx = indexPricedLines(MULTI);
const stats = pricedStatsForPlayer(idx, 'Justin Herbert').map((s) => s.stat).sort();
expect(stats).toEqual(['interceptions', 'pass_td', 'pass_yds']);
});
it('excludeStat drops the already-selected stat (Case A "these OTHERS")', () => {
const idx = indexPricedLines(MULTI);
const stats = pricedStatsForPlayer(idx, 'Justin Herbert', 'pass_yds').map((s) => s.stat).sort();
expect(stats).toEqual(['interceptions', 'pass_td']);
});
it('count comes from the SAME index as the rows (can\'t disagree)', () => {
const idx = indexPricedLines(MULTI);
expect(pricedCountForPlayer(idx, 'Justin Herbert')).toBe(3);
});
it('count is 0 for an off-slate player → caller degrades, never "0 PROPS"', () => {
const idx = indexPricedLines(MULTI);
expect(pricedCountForPlayer(idx, 'Nobody Here')).toBe(0);
});
it('totalPricedCount is the board-wide prop count (real exhaust or 0)', () => {
expect(totalPricedCount(indexPricedLines(MULTI))).toBe(4);
expect(totalPricedCount(indexPricedLines([]))).toBe(0);
expect(totalPricedCount(null)).toBe(0);
});
it('nearestPricedLine picks the closest priced line to a typed line', () => {
const idx = indexPricedLines([
{ player_name: 'Z', stat_type: 'points', line: 27.5, direction: 'over', book_odds: -114, fair_odds: -105 },
{ player_name: 'Z', stat_type: 'points', line: 24.5, direction: 'over', book_odds: -120, fair_odds: -110 },
]);
expect(nearestPricedLine(idx, 'Z', 'points', 30.5).line).toBe(27.5);
expect(nearestPricedLine(idx, 'Z', 'points', 24).line).toBe(24.5);
expect(nearestPricedLine(idx, 'Z', 'points', null).line).toBe(24.5); // sorted first
expect(nearestPricedLine(idx, 'Nobody', 'points', 30)).toBeNull();
});
});
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/);
});
});
describe('Session 80 — freshness + reversible gate (scan page wiring)', () => {
const fs = require('fs');
const src = fs.readFileSync(require.resolve('../../web/src/app/scan/page.tsx'), 'utf8');
it('re-fetches on selection change when the held snapshot is stale', () => {
// The Session-79 gap: the fetch depended on [sport] only, so a long-open
// page showed hour-stale priced lines. Now a stale index re-fetches at use.
expect(src).toMatch(/Date\.now\(\) - pricedFetchedAt > PRICED_STALE_MS/);
expect(src).toMatch(/\}, \[selectedPlayer, stat\]\)/);
});
it('also refreshes on window focus for a long-open page', () => {
expect(src).toMatch(/addEventListener\('focus', onFocus\)/);
});
it('STALE_MS matches the /api/snapshot cache (no over-fetching)', () => {
expect(src).toMatch(/PRICED_STALE_MS = 30_000/);
});
it('the whole nudge is behind ONE reversible gate flag', () => {
expect(src).toMatch(/const PRICED_NUDGE_ENABLED = true/);
// the surfaced set is empty when the gate is off → S6 link-only fallback
expect(src).toMatch(/PRICED_NUDGE_ENABLED && pricedIndex && selectedPlayer/);
});
it('clears the index on sport change (never shows the old sport lines)', () => {
expect(src).toMatch(/setPricedIndex\(null\);\s*\n\s*setPricedFetchedAt\(0\);\s*\n\s*refreshPriced\(\);/);
});
});