// VYNDR 2.0 — Phase G systems (Session 38): living layer, i18n/odds, a11y // prefs, paywall/checkout, parlay correlation math. Pure logic runs directly // via the CommonJS modules; the .tsx glue is asserted against source 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 parlay = require('../../web/src/lib/parlayMath'); const odds = require('../../web/src/lib/oddsFormat'); const prefs = require('../../web/src/lib/prefs'); const { liveTick } = require('../../web/src/lib/liveTick'); const { checkoutUrl } = require('../../web/src/lib/checkout'); describe('Phase G.5 — parlay correlation math (§12)', () => { const jokicPts = { player: 'Jokic', team: 'DEN', sport: 'nba', grade: 'A' }; const jokicReb = { player: 'Jokic', team: 'DEN', sport: 'nba', grade: 'A' }; const murray = { player: 'Murray', team: 'DEN', sport: 'nba', grade: 'A' }; const judge = { player: 'Judge', team: 'NYY', sport: 'mlb', grade: 'A' }; const tatum = { player: 'Tatum', team: 'BOS', sport: 'nba', grade: 'A' }; it('encodes the correlation tiers exactly', () => { expect(parlay.getCorrelation(jokicPts, jokicReb)).toBe(0.62); // same player expect(parlay.getCorrelation(jokicPts, murray)).toBe(0.34); // same team+sport expect(parlay.getCorrelation(jokicPts, tatum)).toBe(0.06); // same league expect(parlay.getCorrelation(jokicPts, judge)).toBe(0); // cross-sport }); it('parlayGrade penalizes a correlated stack vs an independent one', () => { const correlated = parlay.parlayGrade([jokicPts, jokicReb]); // 0.62 → penalty const independent = parlay.parlayGrade([judge, tatum]); // cross-sport, no penalty const order = parlay.GRADE_ORDER; expect(order.indexOf(correlated)).toBeGreaterThan(order.indexOf(independent)); expect(independent).toBe('A'); }); it('converts American↔decimal correctly', () => { expect(parlay.amToDec(110)).toBeCloseTo(2.1, 3); expect(parlay.amToDec(-135)).toBeCloseTo(1.741, 2); expect(parlay.decToAm(2.1)).toBe(110); }); it('maps grades to representative odds and combines a slip', () => { expect(parlay.GRADE_ODDS['A+']).toBe(-135); const dec = parlay.combinedDecimal([jokicPts, judge]); // 110 × 110 expect(dec).toBeCloseTo(2.1 * 2.1, 3); expect(parlay.combinedAmerican([])).toBe('—'); expect(parlay.combinedAmerican([jokicPts])).toMatch(/^[+-]\d+$/); }); }); describe('Phase G.2 — odds formatting (§9, the totals-pass-through rule)', () => { it('converts moneylines across formats', () => { expect(odds.fmtOdds('+150', 'decimal')).toBe('2.50'); expect(odds.fmtOdds('-110', 'decimal')).toBe('1.91'); expect(odds.fmtOdds('+150', 'fractional')).toBe('3/2'); expect(odds.fmtOdds('-110', 'implied')).toBe('52%'); expect(odds.fmtOdds('+150', 'american')).toBe('+150'); }); it('treats integer NUMBERS (the grade→odds map) as moneylines', () => { expect(odds.fmtOdds(110, 'american')).toBe('+110'); expect(odds.fmtOdds(-135, 'decimal')).toBe('1.74'); }); it('CRITICAL: totals / lines pass through UNCHANGED', () => { expect(odds.fmtOdds('228.5', 'decimal')).toBe('228.5'); // half-point total expect(odds.fmtOdds('229', 'implied')).toBe('229'); // unsigned integer total expect(odds.fmtOdds('-7.5', 'american')).toBe('-7.5'); // spread expect(odds.fmtOdds(228.5, 'decimal')).toBe(228.5); // non-integer number }); it('region presets set odds format + currency', () => { expect(odds.regionPreset('UK')).toMatchObject({ odds: 'fractional', currency: 'GBP', currencySymbol: '£' }); expect(odds.regionPreset('EU').odds).toBe('decimal'); expect(odds.regionPreset('US').odds).toBe('american'); }); }); describe('Phase G.3 — accessibility prefs (§10)', () => { function fakeEl() { const attrs = {}; return { attrs, setAttribute: (k, v) => { attrs[k] = v; }, removeAttribute: (k) => { delete attrs[k]; }, }; } function fakeStore() { const m = {}; return { getItem: (k) => (k in m ? m[k] : null), setItem: (k, v) => { m[k] = String(v); } }; } it('applyPrefs sets the data-* attributes the CSS layer keys off', () => { const el = fakeEl(); prefs.applyPrefs({ contrast: 'high', motion: 'reduced', text: 'xl', cb: 'on', font: 'readable' }, el); expect(el.attrs['data-contrast']).toBe('high'); expect(el.attrs['data-motion']).toBe('reduced'); expect(el.attrs['data-text']).toBe('xl'); expect(el.attrs['data-cb']).toBe('1'); expect(el.attrs['data-font']).toBe('readable'); }); it('applyPrefs removes attributes for default values', () => { const el = fakeEl(); el.setAttribute('data-contrast', 'high'); prefs.applyPrefs({ contrast: 'off', text: 'base' }, el); expect(el.attrs['data-contrast']).toBeUndefined(); expect(el.attrs['data-text']).toBeUndefined(); }); it('prefs round-trip through storage', () => { const store = fakeStore(); prefs.savePrefs({ contrast: 'high', region: 'UK' }, store); const loaded = prefs.loadPrefs(store); expect(loaded.contrast).toBe('high'); expect(loaded.region).toBe('UK'); expect(loaded.lang).toBe('en'); // defaults merged }); }); describe('Phase G.1 — living-layer tick engine (§8)', () => { beforeEach(() => liveTick.reset()); afterEach(() => liveTick.reset()); it('fans out one tick to all subscribers', () => { let calls = 0; const unsub = liveTick.subscribe(() => { calls += 1; }); liveTick.tick(); liveTick.tick(); expect(calls).toBe(2); unsub(); liveTick.tick(); expect(calls).toBe(2); // unsubscribed }); it('advances tick and creeps the graded count, with a fresh state object', () => { const first = liveTick.state; for (let i = 0; i < 7; i++) liveTick.tick(); expect(liveTick.state.tick).toBe(7); expect(liveTick.state.graded).toBeGreaterThan(first.graded); expect(liveTick.state).not.toBe(first); // new object → React re-renders }); }); describe('Phase G.4 — checkout (§12)', () => { it('maps a plan tier to the checkout URL', () => { expect(checkoutUrl('analyst')).toBe('/api/checkout?tier=analyst'); expect(checkoutUrl('desk')).toBe('/api/checkout?tier=desk'); expect(checkoutUrl('garbage')).toBe('/api/checkout?tier=analyst'); // safe default }); }); describe('Phase G — React glue wired correctly', () => { it('HeartbeatBar consumes the live tick + renders SIGNAL LIVE', () => { const src = read('components/vyndr/LiveLayer.tsx'); expect(src).toContain('useLive'); expect(src).toContain('SIGNAL LIVE'); expect(src).toContain('ekg-track'); expect(src).toContain('count-tick'); // LiveNumber pop }); it('GlobalHosts registers the design globals + applies prefs on mount', () => { const src = read('components/vyndr/GlobalHosts.tsx'); expect(src).toContain('window.__prefs'); expect(src).toContain('window.__goPaywall'); expect(src).toContain('window.__checkout'); expect(src).toContain('applyPrefs'); }); it('Nav mounts the heartbeat, a prefs trigger, and a paywall read-meter', () => { const src = read('components/Nav.tsx'); expect(src).toContain(' { expect(read('app/layout.tsx')).toContain(''); }); });