Files
vyndr/tests/unit/vyndrCoreScreens.test.js
T
builtbykev 2ab2eeaa7d Wire BookComparison to the prop card (display-only, honest states)
BookComparison.tsx was built but UNROUTED (dead). Route it to the GradeResultCard
via a new self-fetching BookComparisonPanel that reads the live /api/books feed
(source:'bookprices' — the snapshot-locked, fenced, byte-identical store).

Contract fix (Review Zero 0.1): books frequently sit at DIFFERENT lines (WNBA DK
21.5 / FD 18.5; MLB 2/3), so BookComparison now renders EACH book's own line
per-row — never one shared header line implying a false same-number comparison.

Honest states: single-book (the common case for MLB) → one book, "One book
posting this prop.", NO crown/second row; multi-book → all books' own line+price,
NONE crowned (BOOK_CROWN_ENABLED=false — no best-price claim, verified live
crowned:false); no books → renders NULL (panel self-hides), never a placeholder.

No regression: only the always-empty inline d.books section was replaced; grade,
projection, PropLine line, and PriceTriplet price are untouched (wiring test
asserts them). Freshness (0.4): bookprices is written in the SAME snapshot that
locks the grade (intraday refresh touches neither gradedAt.line nor bookprices) —
same fresh, no stale-label needed. Web-only → grade byte-identical trivially.
Full suite 3851 green, web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VsztNChZ7vEvSR61AuMhD1
2026-07-29 04:09:34 -04:00

171 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// VYNDR 2.0 — Phase D core screens (Session 35). Adapter LOGIC is exercised
// directly via the CommonJS gradeAdapter; the .tsx screens/components are
// asserted against source text (the plain-JS Jest config has no TS transform).
const fs = require('fs');
const path = require('path');
const WEB = path.join(__dirname, '..', '..', 'web', 'src');
const read = (rel) => fs.readFileSync(path.join(WEB, rel), 'utf8');
const adapter = require('../../web/src/lib/gradeAdapter');
describe('Phase D — grade adapter (engine → §7 contract)', () => {
const base = {
player: 'Nikola Jokic', sport: 'NBA', stat: 'home_runs', line: 26.5, direction: 'over',
grade: 'A', projection: 29.4, confidence: 67, sample_size: 34,
factors: { matchup: 'soft interior', trend: 'hot', usage: 'up', rest: 'rested', pace: 'fast' },
kill_conditions: [{ code: 'BLOWOUT', reason: 'Blowout risk caps minutes' }],
alt_lines: [{ line: 24.5, grade: 'A+' }, { line: 28.5, grade: 'B' }],
};
it('maps direction → side and humanizes the stat', () => {
const out = adapter.mapScanToGradeResult({ ...base, tier: 'desk' });
expect(out.side).toBe('Over');
expect(out.stat).toBe('Home Runs');
expect(out.player).toBe('Nikola Jokic');
});
it('computes a signed percentage edge from projection vs line', () => {
expect(adapter.computeEdge(29.4, 26.5, 'over')).toBeCloseTo(10.9, 1);
expect(adapter.computeEdge(24, 26.5, 'over')).toBeLessThan(0);
expect(adapter.computeEdge(24, 26.5, 'under')).toBeGreaterThan(0);
});
it('flags phosphor confirmed only for A/A+ with strong support', () => {
expect(adapter.isPhosphorConfirmed('A+', 80, 40)).toBe(true);
expect(adapter.isPhosphorConfirmed('A', 50, 35)).toBe(true); // sample carries it
expect(adapter.isPhosphorConfirmed('B', 90, 90)).toBe(false);
expect(adapter.isPhosphorConfirmed('A', 40, 5)).toBe(false);
});
it('FREE tier teases 3 signals, hides kill conditions and the alt ladder', () => {
const out = adapter.mapScanToGradeResult({ ...base, tier: 'free' });
expect(out.signals.length).toBe(3);
// Session 62 (A1-S1) — free gets the pricing-promised LOCKED PREVIEW
// (code-only teaser), not silence.
expect(out.killConditions.every((k) => k.includes('upgrade to see details'))).toBe(true);
expect(out.altLadder).toEqual([]);
});
it('ANALYST tier shows kill conditions + all signals, but no alt ladder', () => {
const out = adapter.mapScanToGradeResult({ ...base, tier: 'analyst' });
expect(out.signals.length).toBe(5);
expect(out.killConditions).toEqual(['Blowout risk caps minutes']);
expect(out.altLadder).toEqual([]);
});
it('DESK tier unlocks the alt line ladder', () => {
const out = adapter.mapScanToGradeResult({ ...base, tier: 'desk' });
expect(out.altLadder).toEqual([{ line: 24.5, grade: 'A+' }, { line: 28.5, grade: 'B' }]);
});
it('defaults missing fields safely', () => {
const out = adapter.mapScanToGradeResult({});
expect(out.grade).toBe('—');
expect(out.side).toBe('Over');
expect(out.signals).toEqual([]);
expect(out.books).toEqual([]);
});
});
describe('Phase D — GradeResultCard (the core moment)', () => {
const src = read('components/vyndr/GradeResultCard.tsx');
it('renders the grade letter at hero size (92116px)', () => {
expect(src).toContain('116');
expect(src).toContain('92');
});
it('uses the intel-surface for the grade hero zone + grade-reveal', () => {
expect(src).toContain('intel-surface');
expect(src).toContain('grade-reveal');
});
it('book comparison is the live BookComparisonPanel — no best-book highlight while the crown is gated OFF', () => {
// The old inline best-book green tint/border is gone: the crown is
// BOOK_CROWN_ENABLED=false, so the card makes NO best-price claim. The grid
// is now the wired BookComparisonPanel (honest breadth, no crown).
expect(src).toContain('<BookComparisonPanel');
expect(src).not.toContain("b.best ? 'rgba(0,212,160,.13)'");
});
it('renders kill conditions with amber border, gated on non-empty', () => {
expect(src).toContain('hasKill');
expect(src).toContain('rgba(255,179,71,.4)');
expect(src).toContain('KILL CONDITIONS');
});
it('self-hides books / alt ladder when empty', () => {
// Book comparison is now the live BookComparisonPanel (fed by /api/books);
// it self-hides (returns null) on honest-absent. Alt ladder still gates on hasAlt.
expect(src).toContain('BookComparisonPanel');
expect(src).toContain('hasAlt');
});
});
describe('Phase D — ProcessingGrade', () => {
const src = read('components/vyndr/ProcessingGrade.tsx');
it('plays the factor-ignite + proc-scan sequence then reveals the card', () => {
expect(src).toContain('factor-ignite');
expect(src).toContain('proc-scan');
expect(src).toContain('<GradeResultCard');
});
});
describe('Phase D — GameCard (Bloomberg best/worst lines)', () => {
const src = read('components/vyndr/GameCard.tsx');
it('renders team abbreviations', () => {
expect(src).toContain('g.away.abbr');
expect(src).toContain('g.home.abbr');
});
it('best line = green tint + green border; worst = subtle red', () => {
expect(src).toContain('rgba(0,212,160,.13)');
expect(src).toContain('rgba(255,82,82,.07)');
});
it('shows a live-dot for live games and grades props with GradeBadge', () => {
expect(src).toContain('live-dot');
expect(src).toContain('<GradeBadge');
});
});
describe('Phase D — ClaimMeter', () => {
const src = read('components/vyndr/ClaimMeter.tsx');
it('renders the amber founder-seat scarcity bar', () => {
expect(src).toContain('CLAIMED');
expect(src).toContain('var(--amber)');
});
});
// Session 57 (Phase 0) — the Terminal surface was fabricated sample data
// (Finals-era NBA content rendered as live intelligence). The route now
// redirects to /dashboard; the layouts survive UNROUTED as §12 templates.
describe('Phase 0 — Terminal retired (fabricated surface)', () => {
it('the route redirects to the slate instead of rendering fake intel', () => {
const src = read('app/terminal/page.tsx');
expect(src).toContain("redirect('/dashboard')");
expect(src).not.toContain('INJURY_WIRE');
expect(src).not.toContain('Wembanyama');
});
it('preserves the VVI / cascade / leaders layouts as unrouted templates', () => {
const tpl = read('components/intel/TerminalTemplates.tsx');
expect(tpl).toContain('NOT ROUTED');
expect(tpl).toContain('VOLATILITY INDEX');
expect(tpl).toContain('INJURY WIRE');
expect(tpl).toContain('GRADEABLE LEADERS');
});
});
describe('Phase D — scan + landing wiring', () => {
it('scan renders the new ProcessingGrade card via the adapter (legacy GradeCard removed)', () => {
const src = read('app/scan/page.tsx');
expect(src).toContain('ProcessingGrade');
expect(src).toContain('mapScanToGradeResult');
expect(src).not.toContain("from '@/components/GradeCard'");
});
it('scan keeps sportsbook deep-links safe (A1 S3: BOOK_LINK_REL = sponsored noopener noreferrer)', () => {
const src = read('app/scan/page.tsx');
expect(src).toContain('rel={BOOK_LINK_REL}');
// the constant itself carries noopener noreferrer (+ sponsored)
const { BOOK_LINK_REL } = require('../../web/src/lib/bookLinks');
expect(BOOK_LINK_REL).toContain('noopener noreferrer');
});
it('landing mounts the ClaimMeter', () => {
expect(read('app/page.tsx')).toContain('ClaimMeter');
});
});