911cae4992
TASK 1 — Parlay Lab (/parlay): a dedicated Correlation Builder with a
leg source INDEPENDENT of the live slate. Browses tonight's pre-graded
props from /api/snapshot/:sport (resolves players via /api/players/search),
adds legs through useParlay().addLeg (deduped by legKey), and renders the
PARLAY SLIP — combined grade, correlation, and payout read straight off
ParlayContext. Surfaces parlayService's correlation warning as the
CAUTION · CORRELATION FLAG, honors the tier leg-cap (free 2 / analyst 4 /
desk 6) and blurs the payout for free tier with the __goPaywall upsell.
Added /parlay to OPEN_ROUTES (free funnel, like scan/dashboard). Retired
the cleanly-dead ParlayTray.tsx (unmounted since Session 50). No new
proxies — reuses existing snapshot/search/parlay-grade endpoints.
TASK 2 — Live Grade-Shift timeline: web/src/lib/gradeShift.js (pure,
testable) builds a line/grade-movement timeline from already-emitted data
(intraday {t,line} history + revised_from_grade). Color law mirrors
ROW-GRAMMAR / StatStrip.LineSparkline: green = toward the graded side,
amber = against, dim = flat (never red). GradeShift.tsx renders it, shows
the original grade struck-through on a revision, and self-hides below 3
real points. Mounted in GradeResultCard (self-hides on the scan path,
which carries no captured history — honest, never fabricated).
Tests: tests/unit/gradeShift.test.js (15) + tests/unit/parlayLab.test.js
(13). Full suite 245 suites / 2989 tests green (baseline 243/2961).
Next build EXIT=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
88 lines
3.2 KiB
JavaScript
88 lines
3.2 KiB
JavaScript
// Wave 4B — Parlay Lab page (/parlay). Source-asserts the dedicated builder:
|
|
// an independent (non-slate) leg source, useParlay/legKey wiring, the
|
|
// correlation-flag caution, and the tier-aware leg cap.
|
|
|
|
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');
|
|
|
|
describe('Parlay Lab page exists at /parlay', () => {
|
|
it('the route file is present', () => {
|
|
expect(fs.existsSync(path.join(WEB, 'app', 'parlay', 'page.tsx'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('/parlay — independent leg source (not the live slate)', () => {
|
|
const src = read('app/parlay/page.tsx');
|
|
it('browses pre-graded props from /api/snapshot/:sport', () => {
|
|
expect(src).toContain('/api/snapshot/');
|
|
});
|
|
it('resolves players via /api/players/search (per sport)', () => {
|
|
expect(src).toContain('/api/players/search');
|
|
});
|
|
it('does NOT depend on the live Slate component', () => {
|
|
expect(src).not.toContain("from '@/components/Slate'");
|
|
expect(src).not.toContain('slateAdapter');
|
|
});
|
|
it('offers the three parlay sports', () => {
|
|
for (const s of ['MLB', 'NBA', 'WNBA']) expect(src).toContain(s);
|
|
});
|
|
});
|
|
|
|
describe('/parlay — assembles a slip via useParlay + legKey', () => {
|
|
const src = read('app/parlay/page.tsx');
|
|
it('imports useParlay and legKey', () => {
|
|
expect(src).toContain('useParlay');
|
|
expect(src).toContain('legKey');
|
|
});
|
|
it('adds legs through addLeg and dedupes/toggles by leg key', () => {
|
|
expect(src).toContain('addLeg(');
|
|
expect(src).toContain('hasLeg(');
|
|
expect(src).toContain('removeLeg(');
|
|
});
|
|
it('renders the PARLAY SLIP with combined grade + correlation + payout from context', () => {
|
|
expect(src).toContain('PARLAY SLIP');
|
|
expect(src).toContain('combined');
|
|
expect(src).toContain('correlation');
|
|
expect(src).toContain('payout');
|
|
});
|
|
});
|
|
|
|
describe('/parlay — correlation-flag caution', () => {
|
|
const src = read('app/parlay/page.tsx');
|
|
it('surfaces parlayService correlation warning as a caution flag', () => {
|
|
expect(src).toContain('correlation?.warning');
|
|
expect(src).toContain('CORRELATION FLAG');
|
|
});
|
|
});
|
|
|
|
describe('/parlay — tier-aware leg cap + free upsell', () => {
|
|
const src = read('app/parlay/page.tsx');
|
|
it('drives the leg cap from the user tier (free 2 / analyst 4 / desk 6)', () => {
|
|
expect(src).toContain('useAuth');
|
|
expect(src).toContain('setMaxLegs');
|
|
expect(src).toContain('free: 2');
|
|
expect(src).toContain('desk: 6');
|
|
});
|
|
it('honors atCap and blurs the payout for free tier with the paywall upsell', () => {
|
|
expect(src).toContain('atCap');
|
|
expect(src).toContain('__goPaywall');
|
|
expect(src).toContain("blur(");
|
|
});
|
|
});
|
|
|
|
describe('/parlay is reachable (open route, not gated)', () => {
|
|
const routes = require('../../web/src/lib/routes.js');
|
|
it('is an OPEN route (free funnel, like scan/dashboard)', () => {
|
|
expect(routes.OPEN_ROUTES).toContain('/parlay');
|
|
expect(routes.isGatedRoute('/parlay')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('legacy ParlayTray is retired', () => {
|
|
it('the dead component file is gone', () => {
|
|
expect(fs.existsSync(path.join(WEB, 'components', 'ParlayTray.tsx'))).toBe(false);
|
|
});
|
|
});
|