Files
vyndr/tests/unit/vyndrPhaseE.test.js
T
builtbykev bf7c0a3c08 Wave 3: /compare built (real head-to-head); resolution tail scoped, not shipped
No grade, ledger or scoring change. Push scoring untouched.

REVIEW ZERO 0.3/0.4 — THE RESOLUTION TAIL DOES NOT FIRE. The resolver is
POST /api/grading/resolve (routes/grading.js:208), and its fanout at :356-371
covers webPush, telegram and discord — but:

  - share-card generation: SPEC'D-NOT-BUILT. Not in the fanout at all (grep
    shareCard in grading.js = 0). shareCards/renderer.js exists with ZERO
    callers, so the component is built but no step would ever invoke it.
  - push notifications: BUILT-NOT-FIRING. In the fanout but gated on
    webPush.configured() (VAPID). push_subscriptions = 0 rows and
    user_notifications = 0 rows — nothing ever subscribed or delivered.
  - Telegram result posts: BUILT-NOT-FIRING (gated on BOT_TOKEN + CHANNEL_ID).
  - Discord result posts: BUILT-NOT-FIRING (gated on webhookFor('results')).
  - recap (all-Final trigger): SPEC'D-NOT-BUILT. No recap file exists in src/.

AND THE WHOLE TAIL IS UNREACHABLE: nothing calls /api/grading/resolve — there is
no ESPN poller in the repo. The live settlement path is the scheduler's
settleAllOutcomes + settleAllLedgers, which fans out to opsNotify only (ops
alerts), with no user-facing output. So even the built channels have no trigger.

Per the order's own rule, ShareCard, /notifications, result posts and recap are
therefore ALL SCOPED, none shipped — no dead shells over a silent pipeline.

BUILT — /compare. Semantics (0.2): a same-market head-to-head, two players with
every row a measure BOTH sides are scored on, aligned via alignRows so the
numbers are comparable — deliberately not two disconnected graded props. Reads
the live /api/stats/player/:name?sport= aggregate. Honest-absent three ways: an
unresolved side reads NO DATA while the other still renders; a measure only one
side has renders a dash, never 0; if neither resolves the page refuses to
compare. NO VERDICT — it shows measures and says the reader draws the call.

Two pre-existing tests (vyndrPhaseE, vyndrParityQA) asserted the in-development
placeholder; both superseded rather than deleted — they now assert the stronger
properties against the real page (live fetch, no sample players, NO VERDICT,
NO DATA, "not a zero").

Floor: 316 suites / 3930 tests green (10 new), web build exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
2026-07-31 05:31:56 -04:00

123 lines
5.4 KiB
JavaScript

// VYNDR 2.0 — Phase E remaining screens (Session 36): slate adapter + GameCard
// Bloomberg reskin, the four stubs-turned-real pages, and login/pricing reskins.
// Adapter LOGIC runs directly via the CommonJS module; .tsx is asserted as text.
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 slate = require('../../web/src/lib/slateAdapter');
describe('Phase E.1 — slate adapter (best/worst line detection)', () => {
it('parses American odds to a decimal payout (higher = better)', () => {
expect(slate.parseAmericanOdds('+150')).toBeCloseTo(2.5, 3);
expect(slate.parseAmericanOdds('-110')).toBeCloseTo(1.909, 2);
expect(slate.parseAmericanOdds('garbage')).toBeNull();
expect(slate.parseAmericanOdds(null)).toBeNull();
});
it('marks the best/worst moneyline across books', () => {
const rows = slate.detectBestLines({
dk: { awayML: '+150', homeML: '-170', total: '228.5' },
fd: { awayML: '+140', homeML: '-160', total: '229' },
});
const dk = rows.find((r) => r.book === 'dk');
const fd = rows.find((r) => r.book === 'fd');
expect(dk.bestAway).toBe(true); // +150 pays more than +140
expect(fd.worstAway).toBe(true);
expect(fd.bestHome).toBe(true); // -160 pays more than -170
expect(dk.worstHome).toBe(true);
expect(dk.ou).toBe('O/U 228.5');
});
it('does not mark best/worst for a lone book', () => {
const rows = slate.detectBestLines({ dk: { awayML: '+150', homeML: '-170' } });
expect(rows[0].bestAway).toBe(false);
expect(rows[0].worstAway).toBe(false);
});
it('maps schedule → GameCard contract with abbrs, time, lines', () => {
const cards = slate.mapScheduleToGameCards(
[{ id: 'g1', sport: 'NBA', status: 'in', score: { away: 50, home: 48 }, awayTeam: { abbreviation: 'LAL', name: 'Lakers' }, homeTeam: { abbreviation: 'SA', name: 'Spurs' }, gameTime: null }],
{ g1: { books: { dk: { awayML: '+120', homeML: '-140' }, fd: { awayML: '+110', homeML: '-130' } } } },
[{ player: 'LeBron', team: 'LAL', text: '5-game 25+ streak' }],
[{ player: 'LeBron', team: 'LAL', stat: 'Points', line: 26.5, grade: 'A', side: 'Over' }],
);
expect(cards).toHaveLength(1);
expect(cards[0].id).toBe('g1');
expect(cards[0].live).toBe(true);
expect(cards[0].away.abbr).toBe('LAL');
expect(cards[0].lines.length).toBe(2);
expect(cards[0].props.length).toBe(1);
expect(cards[0].streaks.length).toBe(1);
});
it('degrades gracefully when a game has no lines', () => {
const cards = slate.mapScheduleToGameCards(
[{ id: 'g2', awayTeam: { abbreviation: 'NYK' }, homeTeam: { abbreviation: 'BOS' } }],
{}, [], [],
);
expect(cards[0].lines).toEqual([]);
expect(cards[0].live).toBe(false);
});
});
describe('Phase E.1 — GameCard reskin (Bloomberg best/worst)', () => {
const src = read('components/GameCard.tsx');
it('uses the slate adapter best-line detection + new components', () => {
expect(src).toContain('detectBestLines');
expect(src).toContain('SportBadge');
expect(src).toContain('SectionHead');
});
it('renders best = green tint + green border, worst = subtle red', () => {
expect(src).toContain('rgba(0,212,160,.13)');
expect(src).toContain('rgba(255,82,82,.07)');
});
it('dropped the emoji sport marker', () => {
expect(src).not.toContain('SPORT_EMOJI');
});
});
describe('Phase E.3 — stubs are now real pages', () => {
const pages = ['compare', 'invite', 'help', 'about'];
it.each(pages)('/%s is no longer a RouteStub', (p) => {
expect(read(`app/${p}/page.tsx`)).not.toContain('RouteStub');
});
// SUPERSEDED 2026-07-31 by WAVE 3: /compare is now the REAL head-to-head, so the
// in-development placeholder is correctly gone. The honesty properties it guarded
// (no sample players, no fake verdict) are asserted here in their stronger form —
// against a page that fetches live data rather than one that renders nothing.
it('compare is a real head-to-head with no sample grades and no fake verdict', () => {
const src = read('app/compare/page.tsx');
expect(src).not.toMatch(/IN DEVELOPMENT/i);
expect(src).not.toContain('VYNDR VERDICT');
expect(src).not.toMatch(/Joki|Wembanyama/); // no hardcoded sample players
expect(src).toMatch(/\/api\/stats\/player\//); // reads the live feed
expect(src).toMatch(/NO VERDICT/); // shows measures, renders no call
});
it('invite renders the 3-friends referral messaging', () => {
expect(read('app/invite/page.tsx')).toContain('3 friends');
});
it('help renders a searchable FAQ', () => {
const src = read('app/help/page.tsx');
expect(src).toContain('TerminalInput');
expect(src).toContain('FAQ');
});
it('about uses the system-voice brand line', () => {
expect(read('app/about/page.tsx')).toContain('give it back');
});
});
describe('Phase E.2 — login + pricing reskins', () => {
it('login uses scanlines, system voice, and the new Wordmark', () => {
const src = read('app/login/page.tsx');
expect(src).toContain('scanlines');
expect(src).toContain('ACCESS THE SIGNAL');
expect(src).toContain("from '@/components/vyndr'");
});
it('pricing mounts the ClaimMeter', () => {
expect(read('app/pricing/page.tsx')).toContain('ClaimMeter');
});
});