f5156dd16d
Two Truth-Law fixes found by auditing the product logged-out. FIX 1 — /u/[handle] claimed a "CLV-verified record" with "closing-line value included" while ZERO closing-line value renders there. Verified live: GET /api/profiles/vyndr returns beat_close_pct null (gated behind CLV_CAPTURE_RELIABLE, unset while C4 is open). Eight instances found — two of them (the OG + portrait "CLV-VERIFIED RECORD · 30D" eyebrows) only by the post-removal residual sweep; two more printed the claim in exactly the no-record branch. Copy now describes what the page shows. The gated CLV-VERIFIED badge and the BEAT CLOSE figure are removed from the public profile, OG card and portrait card. DISPLAY ONLY: beat_close_pct, clvCaptureReliable() and the whole CLV data path are untouched, and the earned directional badge stays Analyst+Desk. The claim returns when CLV genuinely renders here. Also fixes the doubled "· VYNDR · VYNDR" title (layout's '%s · VYNDR' template already supplies the suffix); verified on composed output by serving the build and reading the real HTML, not on source. FIX 2 — the player page's FORM was `70 + 4 × (count of tonight's graded props)`. Nothing on the HTTP path ever sets stats.form, so that fallback WAS the live number: Josh Bell's "74" is 70 + 4×1 prop, confirmed against his live payload. MATCHUP was gradeFromForm(that number), with a hardcoded 'B' on the no-archetype branch — both fabricated letters with no opponent input on the path. Systemic: buildIntel is the unconditional path for every player and sport. FORM and MATCHUP now render "—" (kind 'plain', so no bar width or colour is computed off a null). gradeFromForm is deleted and the prop count is no longer passed into buildIntel. computeFormScore's hardcoded 75 now returns undefined. Induced across MLB/NBA/WNBA: all render cleanly, and real values (USAGE 3.6 AB/G, REST B2B) still render. Neither form value feeds the grade — engine1 reads raw l5_avg/l20_avg against the line and never a form key; buildIntelFields decorates the already-graded object. Grade inputs are byte-identical. Held (needs a per-sport headline-stat design call): a real player-level form metric + label disambiguation. Tests 3491 passed / 289 suites, web build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
131 lines
5.9 KiB
JavaScript
131 lines
5.9 KiB
JavaScript
// A1 Session 10 — public ledger profiles: /u/[handle] page + OG card +
|
|
// settings claim/publish UI + proxies + migration. The .tsx surfaces are
|
|
// asserted against source (plain-JS Jest, no TS transform) — same pattern as
|
|
// vyndrAppShell/socialPreview.
|
|
|
|
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 exists = (rel) => fs.existsSync(path.join(WEB, rel));
|
|
|
|
const routes = require('../../web/src/lib/routes');
|
|
|
|
describe('/u routing — the share surface stays public', () => {
|
|
it('never gates /u (a public record must load anonymous)', () => {
|
|
expect(routes.isGatedRoute('/u')).toBe(false);
|
|
expect(routes.isGatedRoute('/u/kev')).toBe(false);
|
|
});
|
|
it('declares /u in OPEN_ROUTES', () => {
|
|
expect(routes.OPEN_ROUTES).toContain('/u');
|
|
});
|
|
});
|
|
|
|
describe('/u/[handle] server shell', () => {
|
|
const src = read('app/u/[handle]/page.tsx');
|
|
// Session 65 — UN-CLAIM. The metadata claimed a CLV verification that never
|
|
// renders on this surface (beat_close_pct is null until C4 is fixed). Copy
|
|
// must describe exactly what the page shows and imply no closing-line value.
|
|
it('carries honest settled-record metadata and claims no CLV', () => {
|
|
expect(src).toContain('Settled record — @');
|
|
expect(src).toContain('wins and misses');
|
|
expect(src).not.toMatch(/CLV-verified|closing-line value included/);
|
|
});
|
|
it('does NOT repeat the layout title template suffix (avoids "· VYNDR · VYNDR")', () => {
|
|
// layout.tsx metadata template is '%s · VYNDR' — the page title must not add its own.
|
|
expect(src).not.toMatch(/const title = `[^`]*· VYNDR`/);
|
|
});
|
|
it('renders the client record component', () => {
|
|
expect(src).toContain('PublicProfile');
|
|
expect(exists('app/u/[handle]/PublicProfile.tsx')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('/u/[handle] client record', () => {
|
|
const src = read('app/u/[handle]/PublicProfile.tsx');
|
|
it('honors the n-gate: RECORD BUILDING under min_sample, never a small-sample %', () => {
|
|
expect(src).toContain('RECORD BUILDING');
|
|
expect(src).toMatch(/settled >= minSample/);
|
|
});
|
|
it('shows outcome chips on settled rows, and NO CLV claim (Session 65 un-claim)', () => {
|
|
expect(src).toContain('✓ HIT');
|
|
expect(src).toContain('✕ MISS');
|
|
// The BEAT CLOSE figure and the ✓ CLV-VERIFIED badge are removed from this
|
|
// public surface — both derived from the legacy overwritable closing_line.
|
|
expect(src).not.toContain('BEAT CLOSE');
|
|
expect(src).not.toContain('CLV-VERIFIED');
|
|
});
|
|
it('states the uncurated contract and one not-found state for unknown AND unpublished', () => {
|
|
expect(src).toContain('Nothing curated');
|
|
expect(src).toContain('private by default');
|
|
expect(src).toMatch(/notfound/);
|
|
});
|
|
it('fetches through the Next proxy (S25 rule)', () => {
|
|
expect(src).toContain('/api/profiles/');
|
|
});
|
|
});
|
|
|
|
describe('/u/[handle] OG image', () => {
|
|
const og = read('app/u/[handle]/opengraph-image.tsx');
|
|
it('is a 1200x630 PNG record card', () => {
|
|
expect(og).toContain('width: 1200, height: 630');
|
|
expect(og).toContain("contentType = 'image/png'");
|
|
expect(og).toContain('PUBLIC LEDGER');
|
|
// Session 65 — the no-record fallback tagline, un-claimed (was
|
|
// "CLV-verified record · every settled read · misses included").
|
|
expect(og).toContain('Every settled read · wins and misses · nothing curated');
|
|
});
|
|
it('does NOT use the edge runtime (self-hosted standalone, S53 rule)', () => {
|
|
expect(og).not.toContain("runtime = 'edge'");
|
|
});
|
|
});
|
|
|
|
describe('settings — claim handle + publish toggle', () => {
|
|
const src = read('app/settings/page.tsx');
|
|
it('carries the explicit private-by-default publish copy verbatim', () => {
|
|
expect(src).toContain('Publishing puts your ENTIRE settled record on a public page — wins and misses. Private by default.');
|
|
});
|
|
it('reads and writes /api/profiles/me with the bearer token', () => {
|
|
expect(src).toContain("fetch('/api/profiles/me'");
|
|
expect(src).toMatch(/method: 'POST'[\s\S]*?\/api\/profiles\/me|\/api\/profiles\/me[\s\S]*?method: 'POST'/);
|
|
});
|
|
it('sanitizes the handle input to the API shape (a-z 0-9 _ , max 20)', () => {
|
|
expect(src).toContain("replace(/[^a-z0-9_]/g, '')");
|
|
expect(src).toContain('.slice(0, 20)');
|
|
});
|
|
});
|
|
|
|
describe('Next proxies (S25 rule — Express is not browser-reachable)', () => {
|
|
it('me proxy forwards GET/POST with Authorization', () => {
|
|
const src = read('app/api/profiles/me/route.ts');
|
|
expect(src).toContain('BACKEND_URL');
|
|
expect(src).toContain('/api/profiles/me');
|
|
expect(src).toContain('authorization');
|
|
expect(src).toContain('export async function POST');
|
|
});
|
|
it('handle proxy forwards the public read and passes the 404 through untouched', () => {
|
|
const src = read('app/api/profiles/[handle]/route.ts');
|
|
expect(src).toContain('BACKEND_URL');
|
|
expect(src).toContain('/api/profiles/');
|
|
expect(src).toContain('status: upstream.status');
|
|
});
|
|
});
|
|
|
|
describe('migration 022 — public_profiles (committed, applied by the founder)', () => {
|
|
const sql = fs.readFileSync(path.join(ROOT, 'supabase', 'migrations', '022_public_profiles.sql'), 'utf8');
|
|
it('creates the table with the handle CHECK + private-by-default published', () => {
|
|
expect(sql).toContain('public.public_profiles');
|
|
expect(sql).toContain("handle ~ '^[a-z0-9_]{3,20}$'");
|
|
expect(sql).toMatch(/published\s+boolean NOT NULL DEFAULT false/);
|
|
expect(sql).toContain('REFERENCES auth.users');
|
|
});
|
|
it('RLS: published rows readable by anyone, own row by owner, writes service-role only', () => {
|
|
expect(sql).toContain('ENABLE ROW LEVEL SECURITY');
|
|
expect(sql).toContain('USING (published = true)');
|
|
expect(sql).toContain('USING (user_id = auth.uid())');
|
|
expect(sql).not.toMatch(/FOR (INSERT|UPDATE|DELETE)/);
|
|
});
|
|
});
|