Files
vyndr/tests/unit/playerIntelService.test.js
T
builtbykev f5156dd16d Un-claim CLV on the public profile; un-fabricate player-page FORM
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
2026-07-20 17:05:04 -04:00

149 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Session 42 — player intelligence aggregation. cacheGet is injected so these
// run pure (no Redis, no HTTP, no rate limiter).
const svc = require('../../src/services/playerIntelService');
const cacheWith = (envelope) => async (key) => (key.startsWith('grades:') ? envelope : null);
describe('sanitizePlayerName', () => {
it('decodes URL encoding and keeps name punctuation', () => {
expect(svc.sanitizePlayerName('Luka%20Doncic')).toBe('Luka Doncic');
expect(svc.sanitizePlayerName("De'Aaron Fox")).toBe("De'Aaron Fox");
expect(svc.sanitizePlayerName('Ronald Acuna Jr.')).toBe('Ronald Acuna Jr'); // S46: suffix de-dotted
});
it('strips injection / control characters', () => {
expect(svc.sanitizePlayerName('Luka<script>')).toBe('Lukascript');
expect(svc.sanitizePlayerName('a/../../etc/passwd')).toBe('aetcpasswd'); // S46: periods stripped
expect(svc.sanitizePlayerName('x'.repeat(200)).length).toBe(60);
});
it('handles malformed percent-encoding without throwing', () => {
expect(() => svc.sanitizePlayerName('%E0%A4%A')).not.toThrow();
});
});
describe('getPlayerIntel', () => {
it('returns archetype + intelligence + props, found=true when player has grades', async () => {
const envelope = {
grades: [
{ player: 'Austin Riley', team: 'ATL', stat_type: 'total_bases', line: 1.5, direction: 'over', grade: 'B+', confidence: 71 },
{ player: 'Austin Riley', stat_type: 'home_runs', line: 0.5, direction: 'over', grade: 'C', confidence: 60 },
{ player: 'Someone Else', stat_type: 'hits', line: 1.5, direction: 'over', grade: 'A', confidence: 80 },
],
};
const r = await svc.getPlayerIntel('Austin Riley', 'mlb', {
cacheGet: cacheWith(envelope),
stats: { avg: 0.282, hr: 18, rbi: 54, ops: 0.845, k_rate: 26 },
});
expect(r.player).toBe('Austin Riley');
expect(r.sport).toBe('mlb');
expect(r.team).toBe('ATL');
expect(r.found).toBe(true);
expect(r.archetype.primary).toBeTruthy();
expect(r.activeProps).toHaveLength(2); // only Riley's two props
expect(r.activeProps[0]).toMatchObject({ stat: 'total_bases', side: 'O', grade: 'B+' });
expect(r.propDNA.reliable.length + r.propDNA.volatile.length).toBeGreaterThan(0);
expect(r.education.length).toBeGreaterThan(0);
expect(r.intel.find((m) => m.label === 'FORM')).toBeTruthy();
});
it('degrades gracefully when the grades cache is cold (found=false, no crash)', async () => {
const r = await svc.getPlayerIntel('Nobody Special', 'nba', { cacheGet: async () => null });
expect(r.found).toBe(false);
expect(r.activeProps).toEqual([]);
expect(r.archetype.primary).toBeTruthy(); // fallback archetype
expect(Array.isArray(r.intel)).toBe(true);
});
it('survives a throwing cache (returns a valid payload)', async () => {
const r = await svc.getPlayerIntel('X', 'nba', { cacheGet: async () => { throw new Error('redis down'); } });
expect(r.activeProps).toEqual([]);
expect(r.player).toBe('X');
});
});
describe('getLeaders', () => {
const envelope = {
grades: [
{ player: 'A', stat_type: 'hits', line: 1.5, direction: 'over', grade: 'A', confidence: 88 },
{ player: 'B', stat_type: 'hits', line: 1.5, direction: 'over', grade: 'B', confidence: 72 },
{ player: 'C', stat_type: 'home_runs', line: 0.5, direction: 'over', grade: 'A', confidence: 95 },
],
};
it('returns top props by confidence', async () => {
const r = await svc.getLeaders('mlb', { cacheGet: cacheWith(envelope), limit: 2 });
expect(r).toHaveLength(2);
expect(r[0].player).toBe('C'); // highest confidence
expect(r[0].confidence).toBe(95);
});
it('filters to a single stat when given', async () => {
const r = await svc.getLeaders('mlb', { cacheGet: cacheWith(envelope), stat: 'hits' });
expect(r).toHaveLength(2);
expect(r.every((x) => x.stat === 'hits')).toBe(true);
});
it('returns [] on a cold cache', async () => {
expect(await svc.getLeaders('nba', { cacheGet: async () => null })).toEqual([]);
});
});
// ── Session 65 — UN-FABRICATE (Truth Law) ────────────────────────────────
// FORM used to be `70 + 4 × (count of tonight's graded props)` whenever
// `stats.form` was absent — and nothing on the HTTP path ever sets it, so that
// fallback WAS the live number (Josh Bell's "74" = 70 + 4×1 prop). MATCHUP was
// `gradeFromForm(that number)`, with a hardcoded 'B' when no archetype
// resolved: both branches fabricated a letter with zero opponent input.
describe('buildIntel — absent renders absent, never a manufactured number', () => {
const { buildIntel } = svc._internals;
const arch = { primary: { name: 'BOMBER' } };
it('renders FORM as "—" with no bar when no real form value exists', () => {
const form = buildIntel({}, arch).find((m) => m.label === 'FORM');
expect(form.value).toBe('—');
expect(form.kind).toBe('plain'); // 'plain' draws no progress bar → no width math on a null
expect(form.score).toBeUndefined();
});
it('renders MATCHUP as "—", never a grade letter, with no opponent input', () => {
const m = buildIntel({}, arch).find((x) => x.label === 'MATCHUP');
expect(m.value).toBe('—');
expect(m.kind).toBe('plain');
// and not merely because the archetype was missing — the old code
// hardcoded 'B' on that branch too.
const noArch = buildIntel({}, { primary: null }).find((x) => x.label === 'MATCHUP');
expect(noArch.value).toBe('—');
});
it('the prop count can NEVER move FORM (the fabrication that shipped)', async () => {
const mk = (n) => ({
grades: Array.from({ length: n }, (_, i) => ({
player: 'Josh Bell', stat_type: `stat_${i}`, line: 0.5, direction: 'over', grade: 'B', confidence: 57,
})),
});
const formFor = async (n) => {
const r = await svc.getPlayerIntel('Josh Bell', 'mlb', { cacheGet: cacheWith(mk(n)), stats: {} });
return r.intel.find((m) => m.label === 'FORM').value;
};
expect(await formFor(1)).toBe('—'); // was '74'
expect(await formFor(5)).toBe('—'); // was '90'
expect(await formFor(0)).toBe('—'); // was '70'
});
it('still renders a REAL form value when one is supplied (un-claim, not un-build)', () => {
const form = buildIntel({ form: 63 }, arch).find((m) => m.label === 'FORM');
expect(form.value).toBe('63');
expect(form.kind).toBe('form');
expect(form.score).toBe('63%');
});
it('usage and rest keep their honest absent states', () => {
const rows = buildIntel({}, arch);
expect(rows.find((m) => m.label === 'USAGE').value).toBe('—');
expect(rows.find((m) => m.label === 'REST').value).toBe('—');
expect(buildIntel({ usage: '3.6 AB/G', rest: 'B2B' }, arch).find((m) => m.label === 'USAGE').value).toBe('3.6 AB/G');
});
});