4b262fdd65
- /responsible-gambling rebuilt sincerely: 21+, 1-800-GAMBLER primary, 17-state resource list, warning signs, self-exclusion guidance, links to the existing /settings surfaces. No marketing adjacency. - /terms + /privacy honest drafts with entity placeholders ([ENTITY NAME], [STATE OF FORMATION], [ARBITRATION VENUE], [CONTACT EMAIL]); privacy sub-processors match reality (adds Sentry, honest PostHog description). - NEW /methodology: pipeline -> engine -> letter grades, refusals, VYNDR Originals, ledger settle + CLV + n>=20, why misses are public. - /about audited to North Star framing (THE PROOF card, methodology link). - Footer: 1-800-GAMBLER + Methodology link; compliance audit found zero pages suppressing the global footer. - 5 Ghost seed articles in content/articles/ + docs/GHOST-PUBLISHING.md manual runbook (no Ghost access used). - tests/unit/compliancePages.test.js (repo source-text style). 2429 tests green, web build exit 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
190 lines
7.1 KiB
JavaScript
190 lines
7.1 KiB
JavaScript
// Session 2 (A1 board) — compliance + approval pack. The legal/trust surfaces
|
|
// are asserted against their source text (plain-JS Jest config, no TS
|
|
// transform — same pattern as vyndrAppShell.test.js).
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const ROOT = path.join(__dirname, '..', '..');
|
|
const WEB = path.join(ROOT, 'web', 'src');
|
|
const read = (rel) => fs.readFileSync(path.join(WEB, rel), 'utf8');
|
|
const readRoot = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
const exists = (rel) => fs.existsSync(path.join(WEB, rel));
|
|
|
|
describe('S2 — /methodology page', () => {
|
|
it('exists as a server component with metadata', () => {
|
|
expect(exists('app/methodology/page.tsx')).toBe(true);
|
|
const src = read('app/methodology/page.tsx');
|
|
expect(src).not.toContain("'use client'");
|
|
expect(src).toContain('export const metadata');
|
|
});
|
|
|
|
const src = read('app/methodology/page.tsx');
|
|
|
|
it('covers the pipeline → engine → letter grades', () => {
|
|
expect(src).toContain('The pipeline');
|
|
expect(src).toContain('feature vector');
|
|
expect(src).toMatch(/11-step letter grade/);
|
|
expect(src).toContain('A+');
|
|
});
|
|
|
|
it('covers refusals when the model has no projection', () => {
|
|
expect(src).toContain('When the model refuses');
|
|
expect(src).toContain('refuses to grade');
|
|
expect(src).toContain('Absent beats wrong');
|
|
});
|
|
|
|
it('covers the VYNDR Originals archetype system', () => {
|
|
expect(src).toContain('VYNDR Originals');
|
|
expect(src).toContain('41');
|
|
['TORCH', 'BOMBER', 'GHOST', 'CONDUCTOR'].forEach((a) => expect(src).toContain(a));
|
|
});
|
|
|
|
it('covers ledger settlement: lock at grade time, box-score settle, CLV, n≥20', () => {
|
|
expect(src).toContain('How the Ledger settles');
|
|
expect(src).toContain('locked with the exact line, the odds, and a timestamp');
|
|
expect(src).toContain('real box score');
|
|
expect(src).toContain('closing line');
|
|
expect(src).toContain('n≥20');
|
|
expect(src).toContain('RECORD BUILDING');
|
|
});
|
|
|
|
it('covers why misses are public', () => {
|
|
expect(src).toContain('Why the misses are public');
|
|
expect(src).toContain('same format');
|
|
});
|
|
|
|
it('states the not-a-sportsbook position and links the ledger', () => {
|
|
expect(src).toContain('not a sportsbook');
|
|
expect(src).toContain('/ledger');
|
|
});
|
|
|
|
it('is registered as an open (ungated) route and linked from the footer', () => {
|
|
const routes = require('../../web/src/lib/routes');
|
|
expect(routes.OPEN_ROUTES).toContain('/methodology');
|
|
expect(routes.isGatedRoute('/methodology')).toBe(false);
|
|
expect(read('components/Footer.tsx')).toContain("href: '/methodology'");
|
|
});
|
|
});
|
|
|
|
describe('S2 — /terms and /privacy drafts', () => {
|
|
it('terms page exists with the non-negotiable disclosures', () => {
|
|
const src = read('app/terms/page.tsx');
|
|
expect(src).toContain('not a sportsbook');
|
|
expect(src).toContain('21 years of age');
|
|
expect(src).toContain('Stripe');
|
|
expect(src).toContain('1-800-GAMBLER');
|
|
});
|
|
|
|
it('privacy page exists with the real sub-processor list', () => {
|
|
const src = read('app/privacy/page.tsx');
|
|
expect(src).toContain('never sell');
|
|
['Stripe', 'Supabase', 'PostHog', 'Resend', 'Sentry'].forEach((p) => expect(src).toContain(p));
|
|
});
|
|
|
|
it('entity details are placeholder tokens, not invented facts', () => {
|
|
const terms = read('app/terms/page.tsx');
|
|
const privacy = read('app/privacy/page.tsx');
|
|
['[ENTITY NAME]', '[STATE OF FORMATION]', '[ARBITRATION VENUE]', '[CONTACT EMAIL]'].forEach((t) =>
|
|
expect(terms).toContain(t),
|
|
);
|
|
['[ENTITY NAME]', '[CONTACT EMAIL]'].forEach((t) => expect(privacy).toContain(t));
|
|
// The previous draft asserted an unverified LLC + emails — keep them out
|
|
// until Kev fills the placeholders.
|
|
[terms, privacy].forEach((src) => {
|
|
expect(src).not.toContain('VYNDR Intelligence LLC');
|
|
expect(src).not.toContain('legal@vyndr.app');
|
|
expect(src).not.toContain('privacy@vyndr.app');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('S2 — /responsible-gambling (sincere rebuild)', () => {
|
|
const src = read('app/responsible-gambling/page.tsx');
|
|
|
|
it('carries 1-800-GAMBLER as the primary helpline and the 21+ mark', () => {
|
|
expect(src).toContain('1-800-GAMBLER');
|
|
expect(src).toContain('tel:18004262537');
|
|
expect(src).toContain('21+');
|
|
});
|
|
|
|
it('lists state resources for the major legal states', () => {
|
|
['Michigan', 'New Jersey', 'New York', 'Pennsylvania', 'Ohio', 'Illinois'].forEach((s) =>
|
|
expect(src).toContain(s),
|
|
);
|
|
});
|
|
|
|
it('links the existing self-exclusion / settings surfaces', () => {
|
|
expect(src).toContain('/settings');
|
|
expect(src).toContain('Self-exclusion');
|
|
});
|
|
|
|
it('links national support resources', () => {
|
|
['ncpgambling.org', 'gamblersanonymous.org', 'gamtalk.org'].forEach((r) => expect(src).toContain(r));
|
|
});
|
|
|
|
it('has no marketing adjacency (no pricing/upgrade/scan funnels)', () => {
|
|
['/pricing', '/upgrade', '/scan', '__goPaywall'].forEach((bad) => expect(src).not.toContain(bad));
|
|
});
|
|
});
|
|
|
|
describe('S2 — compliance footer (global)', () => {
|
|
it('the root layout mounts the single global Footer', () => {
|
|
expect(read('app/layout.tsx')).toContain('<Footer />');
|
|
});
|
|
|
|
it('the footer carries 21+, the RG link, and not-a-sportsbook', () => {
|
|
const src = read('components/Footer.tsx');
|
|
expect(src).toContain('21+');
|
|
expect(src).toContain("href: '/responsible-gambling'");
|
|
expect(src).toContain('not a sportsbook');
|
|
expect(src).toContain('1-800-GAMBLER');
|
|
});
|
|
|
|
it('no nested layout replaces the root layout (footer cannot be bypassed)', () => {
|
|
// The only nested layout (player/[name]) must render children straight
|
|
// through — it exists for metadata only.
|
|
const nested = read('app/player/[name]/layout.tsx');
|
|
expect(nested).toContain('return children');
|
|
expect(nested).not.toContain('<html');
|
|
});
|
|
});
|
|
|
|
describe('S2 — seed articles + Ghost runbook', () => {
|
|
const ARTICLES = [
|
|
'content/articles/how-vyndr-grades-a-prop.md',
|
|
'content/articles/the-vyndr-originals.md',
|
|
'content/articles/why-our-misses-are-public.md',
|
|
'content/articles/what-clv-is.md',
|
|
'content/articles/how-streaks-lie.md',
|
|
];
|
|
|
|
it.each(ARTICLES)('%s exists with draft front matter', (rel) => {
|
|
const src = readRoot(rel);
|
|
expect(src.startsWith('---')).toBe(true);
|
|
expect(src).toContain('title:');
|
|
expect(src).toContain('status: draft');
|
|
});
|
|
|
|
it.each(ARTICLES)('%s obeys VOICE v1.1 — zero exclamation points', (rel) => {
|
|
expect(readRoot(rel)).not.toContain('!');
|
|
});
|
|
|
|
it('the new user-facing pages obey VOICE v1.1 — zero exclamation points', () => {
|
|
[
|
|
'app/methodology/page.tsx',
|
|
'app/responsible-gambling/page.tsx',
|
|
'app/about/page.tsx',
|
|
'app/terms/page.tsx',
|
|
'app/privacy/page.tsx',
|
|
].forEach((rel) => expect(read(rel)).not.toContain('!'));
|
|
});
|
|
|
|
it('the Ghost runbook exists and stays manual (no auto-posting)', () => {
|
|
const src = readRoot('docs/GHOST-PUBLISHING.md');
|
|
expect(src).toContain('blog.vyndr.app');
|
|
expect(src).toContain('draft');
|
|
expect(src).toContain('nothing auto-posts');
|
|
});
|
|
});
|