Wave 1: kill three trust bugs (billing renewal + namesake collision + Desk copy)

FIX 1 — Honest billing renewal render. VYNDR tiers are monthly, so a
`subscription_end` far in the future (the manually-seeded "RENEWS 6/9/2036"
founder row) is a comped/lifetime/seed value, not a renewal. New
web/src/lib/billingDisplay.js `classifyRenewal()` → date | none | lapsed |
unknown (strict Date.parse guard, MONTHLY_RENEWAL_MAX_DAYS=60). Profile page
renders the classified label for both the "Renews" stat and the
cancel-scheduled "Access ends" line — no raw far-future date. No DB row mutated.

FIX 2 — MLB namesake collision (James Wood → "Chicago Cubs"). searchPlayer now
collects ALL exact-nameKey matches instead of first-`.find`; a ≥2 collision
resolves ONLY via a confident teamHint (the prop's game participants, matched
against the cached /teams list with ESPN↔statsapi abbr reconciliation), else
refuses (null) — never guesses. The hint threads getPlayerStats →
resolvePlayerStats → snapshotService (built from each prop's home/away team).
Join invariant: a single-exact player whose team isn't in the hinted game has
its team DROPPED (null), so streaks/rosterlogs never tag a foreign team. Full
teamHint recovery shipped (not just the refuse fallback).

FIX 3 — DeskShowcase headline "A $1M terminal." → deadpan value-showing copy
"Every grade, every alt line, live." Prices ($44.99 / $34.99) unchanged.

Tests: billingDisplay.test.js (7), mlbNamesakeResolve.test.js (12,
disambiguation + join invariant + pure helpers), ds5PricingStates updated to
assert the new headline and no "$1M". Full suite green (237 suites / 2863
tests); web `next build` exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-13 03:48:58 -04:00
parent 93a220e0ca
commit b6787af191
11 changed files with 421 additions and 29 deletions
+48
View File
@@ -0,0 +1,48 @@
// Wave 1 trust bug — honest renewal render. classifyRenewal must never let a
// far-future / comped `subscription_end` (the "RENEWS 6/9/2036" lie) render as
// a real monthly renewal, and must fail closed to `unknown` on bad input.
const { classifyRenewal, MONTHLY_RENEWAL_MAX_DAYS } = require('../../web/src/lib/billingDisplay');
const NOW = Date.parse('2026-07-13T00:00:00.000Z');
const DAY = 86_400_000;
describe('classifyRenewal — honest billing render', () => {
test('MONTHLY_RENEWAL_MAX_DAYS is 60', () => {
expect(MONTHLY_RENEWAL_MAX_DAYS).toBe(60);
});
test('the 2036 comped/seed row is NOT a renewal → none', () => {
const r = classifyRenewal('2036-06-09T00:00:00.000Z', NOW);
expect(r.kind).toBe('none');
expect(r.iso).toBeUndefined();
});
test('a plausible monthly next-bill (now + 30d) → date, carries iso', () => {
const r = classifyRenewal(new Date(NOW + 30 * DAY).toISOString(), NOW);
expect(r.kind).toBe('date');
expect(typeof r.iso).toBe('string');
expect(Date.parse(r.iso)).toBe(NOW + 30 * DAY);
});
test('absent value (null / undefined / empty) → unknown', () => {
expect(classifyRenewal(null, NOW).kind).toBe('unknown');
expect(classifyRenewal(undefined, NOW).kind).toBe('unknown');
expect(classifyRenewal('', NOW).kind).toBe('unknown');
});
test('a renewal well in the past → lapsed', () => {
const r = classifyRenewal(new Date(NOW - 10 * DAY).toISOString(), NOW);
expect(r.kind).toBe('lapsed');
});
test('malformed date string → unknown (never coerced to an epoch date)', () => {
expect(classifyRenewal('not a date', NOW).kind).toBe('unknown');
expect(classifyRenewal('N/A', NOW).kind).toBe('unknown');
});
test('exactly at the 60-day boundary still reads as a date; just beyond → none', () => {
expect(classifyRenewal(new Date(NOW + 60 * DAY).toISOString(), NOW).kind).toBe('date');
expect(classifyRenewal(new Date(NOW + 61 * DAY).toISOString(), NOW).kind).toBe('none');
});
});
+6 -2
View File
@@ -47,10 +47,14 @@ describe('Pricing — Desk is the hero, real prices, single primary CTA', () =>
expect(pricing).not.toContain("originalPrice: '$49.99'");
});
test('the "$1M terminal · $44.99" story leads, above the grid, with a real feature ladder', () => {
test('the Desk story leads with deadpan value-showing copy (no "$1M" brag), above the grid, with a real feature ladder', () => {
expect(page).toContain('import DeskShowcase');
expect(page).toContain('<DeskShowcase');
expect(showcase).toContain('$1M terminal');
// Wave 1 — the headline SHOWS what Desk does instead of claiming a dollar
// figure (VYNDR voice: understated, no hype, no "$1M"/"terminal"-as-brag).
expect(showcase).toContain('Every grade, every alt line, live.');
expect(showcase).not.toContain('$1M');
expect(showcase).not.toContain('1M terminal');
expect(showcase).toContain('$44.99'); // the conversion hook figure
expect(showcase).toContain('$34.99'); // the founder price
// real feature visuals fill the right half (kills the dead half, #8)
+141
View File
@@ -0,0 +1,141 @@
// Wave 1 trust bug — MLB namesake collision. Two different players can share an
// EXACT nameKey ("James Wood": the Nationals star + a Cubs-affiliate namesake).
// The old `.find` took the first → wrong currentTeam → wrong opponents → "built
// vs AL East" fabrication in streaks/rosterlogs. Doctrine: never guess among
// namesakes — resolve only with a confident team hint, else refuse (null).
//
// Hermetic: `searchPlayer` accepts injected `people`/`teams` fixtures — no
// network, no redis.
const mlb = require('../../src/services/adapters/mlbStatsAdapter');
const { teamRecordMatchesHint, disambiguateByHint, canonAbbr } = mlb.__internals;
// statsapi-shaped fixtures.
const JUDGE = {
id: 592450, fullName: 'Aaron Judge',
currentTeam: { id: 147, name: 'New York Yankees' },
primaryPosition: { abbreviation: 'RF' },
};
const WOOD_NATIONALS = {
id: 691026, fullName: 'James Wood',
currentTeam: { id: 120, name: 'Washington Nationals' },
primaryPosition: { abbreviation: 'LF' },
};
const WOOD_CUBS = {
id: 999999, fullName: 'James Wood',
currentTeam: { id: 112, name: 'Chicago Cubs' },
primaryPosition: { abbreviation: 'P' },
};
const SOTO_METS = {
id: 665742, fullName: 'Juan Soto',
currentTeam: { id: 121, name: 'New York Mets' },
primaryPosition: { abbreviation: 'RF' },
};
const TEAMS = [
{ id: 147, abbr: 'NYY', name: 'New York Yankees' },
{ id: 120, abbr: 'WSH', name: 'Washington Nationals' },
{ id: 112, abbr: 'CHC', name: 'Chicago Cubs' },
{ id: 146, abbr: 'MIA', name: 'Miami Marlins' },
{ id: 121, abbr: 'NYM', name: 'New York Mets' },
{ id: 119, abbr: 'LAD', name: 'Los Angeles Dodgers' },
{ id: 135, abbr: 'SD', name: 'San Diego Padres' },
];
describe('searchPlayer — namesake disambiguation (never guess)', () => {
test('(a) a single unambiguous name resolves (Aaron Judge → Yankees)', async () => {
const r = await mlb.searchPlayer('Aaron Judge', 2026, { people: [JUDGE], teams: TEAMS });
expect(r).not.toBeNull();
expect(r.id).toBe(592450);
expect(r.team).toBe('New York Yankees');
});
test('(b) two "James Wood" with NO hint → null (NEVER the Cubs one)', async () => {
const r = await mlb.searchPlayer('James Wood', 2026, { people: [WOOD_NATIONALS, WOOD_CUBS], teams: TEAMS });
expect(r).toBeNull(); // refuse — honest absent beats wrong
});
test('(c) two "James Wood" + a Nationals-game hint → the Nationals Wood', async () => {
const r = await mlb.searchPlayer('James Wood', 2026, {
people: [WOOD_CUBS, WOOD_NATIONALS], // Cubs first — the old bug picked this
teams: TEAMS,
teamHint: ['WSH', 'MIA'], // Nationals @ Marlins
});
expect(r).not.toBeNull();
expect(r.id).toBe(691026);
expect(r.team).toBe('Washington Nationals');
expect(r.team).not.toBe('Chicago Cubs');
});
test('(c2) a full-team-name hint disambiguates too', async () => {
const r = await mlb.searchPlayer('James Wood', 2026, {
people: [WOOD_CUBS, WOOD_NATIONALS],
teams: TEAMS,
teamHint: ['Washington Nationals', 'Miami Marlins'],
});
expect(r.id).toBe(691026);
});
test('(c3) two namesakes + a hint matching NEITHER → null (still refuse)', async () => {
const r = await mlb.searchPlayer('James Wood', 2026, {
people: [WOOD_NATIONALS, WOOD_CUBS],
teams: TEAMS,
teamHint: ['LAD', 'SD'],
});
expect(r).toBeNull();
});
});
// JOIN INVARIANT (streaks/rosterlogs) — a resolved player's team must be a
// participant of the prop's game; on mismatch the team is DROPPED, never a
// foreign tag. This is the mechanism snapshotService relies on before it writes
// teamByPlayer + rosterlogs opponents.
describe('searchPlayer — team join invariant (drop, never tag foreign)', () => {
test('a single player whose team is NOT in the hinted game → team dropped to null', async () => {
const r = await mlb.searchPlayer('Juan Soto', 2026, {
people: [SOTO_METS],
teams: TEAMS,
teamHint: ['LAD', 'SD'], // Soto (Mets) is in neither → cannot confirm join
});
expect(r).not.toBeNull(); // still the right player by name
expect(r.id).toBe(665742);
expect(r.team).toBeNull(); // but his team is not tagged onto a foreign game
expect(r.teamId).toBeNull();
});
test('a single player whose team IS in the hinted game keeps its team', async () => {
const r = await mlb.searchPlayer('Juan Soto', 2026, {
people: [SOTO_METS],
teams: TEAMS,
teamHint: ['NYM', 'LAD'], // Mets @ Dodgers → confirmed
});
expect(r.team).toBe('New York Mets');
});
test('with NO hint, a single player keeps its authoritative team', async () => {
const r = await mlb.searchPlayer('Juan Soto', 2026, { people: [SOTO_METS], teams: TEAMS });
expect(r.team).toBe('New York Mets');
});
});
describe('pure helpers — abbr reconciliation + hint matching', () => {
test('ESPN↔statsapi abbr aliases canonicalize (AZ↔ARI, CHW↔CWS)', () => {
expect(canonAbbr('AZ')).toBe(canonAbbr('ARI'));
expect(canonAbbr('CHW')).toBe(canonAbbr('CWS'));
expect(canonAbbr('nyy')).toBe('NYY');
});
test('teamRecordMatchesHint matches by abbr via the teams list', () => {
expect(teamRecordMatchesHint({ id: 120, name: 'Washington Nationals' }, ['WSH'], TEAMS)).toBe(true);
expect(teamRecordMatchesHint({ id: 120, name: 'Washington Nationals' }, ['CHC'], TEAMS)).toBe(false);
});
test('teamRecordMatchesHint matches by partial name', () => {
expect(teamRecordMatchesHint({ id: 120, name: 'Washington Nationals' }, ['Nationals'], TEAMS)).toBe(true);
});
test('disambiguateByHint refuses when 2 candidates share the hinted team', () => {
const dupe = [WOOD_NATIONALS, { ...WOOD_CUBS, currentTeam: { id: 120, name: 'Washington Nationals' } }];
expect(disambiguateByHint(dupe, ['WSH'], TEAMS)).toBeNull();
});
});