Files
vyndr/tests/unit/playerGrouping.test.js
builtbykev db61876b2f P0-3b/c: consensus grouped by player + ledger cards nest alt lines as a ladder
Completes P0-3 across all three surfaces:
- CONSENSUS VS MODEL (MarketBreadth): dedupe by player+market so Ben Williamson's
  alt-line variants show as ONE consensus row (no per-market cap — a consensus
  table just shouldn't repeat a player).
- LEDGER cards: group by player+market via groupIntoLadders — Alec Bohm's
  strikeout ladder (U1.6/U1.3/O1.5) is now ONE card with the rungs nested (each
  its own side/line + tier-colored grade), not three separate cards.
- playerGrouping reads player OR player_name (ledger rows use player_name) —
  regression-tested so the ledger doesn't silently empty.

The Alt Line Ladder shape is what /pricing already demos; the record now uses it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 01:07:28 -04:00

80 lines
3.4 KiB
JavaScript

'use strict';
// P0-3 — player/market grouping. Same player must present as ONE row per market
// family (alt lines nested), and no single market may flood a ranked list.
const { dedupeLeaders, groupIntoLadders, marketFamily } = require('../../web/src/lib/playerGrouping');
describe('dedupeLeaders', () => {
test("collapses a player's alt-line ladder to ONE leaderboard row (Bohm strikeouts)", () => {
const rows = [
{ player: 'Alec Bohm', stat: 'strikeouts', line: 1.6, side: 'U', grade: 'B' },
{ player: 'Alec Bohm', stat: 'strikeouts', line: 1.3, side: 'U', grade: 'B' },
{ player: 'Alec Bohm', stat: 'strikeouts', line: 1.5, side: 'O', grade: 'B' },
{ player: 'Mike Trout', stat: 'hits', line: 0.5, side: 'O', grade: 'A' },
];
const out = dedupeLeaders(rows);
expect(out).toHaveLength(2);
expect(out[0]).toMatchObject({ player: 'Alec Bohm', line: 1.6 }); // first (best-ranked) kept
expect(out[1].player).toBe('Mike Trout');
});
test('caps a flooding market so 9 identical stolen_bases rows do not dominate', () => {
const rows = Array.from({ length: 9 }, (_, i) => ({ player: `Runner ${i}`, stat: 'stolen_bases', line: 0.5, side: 'U', grade: 'B' }));
rows.push({ player: 'Slugger', stat: 'home_runs', line: 0.5, side: 'O', grade: 'A' });
const out = dedupeLeaders(rows, 4);
expect(out.filter((r) => r.stat === 'stolen_bases')).toHaveLength(4); // capped
expect(out.some((r) => r.stat === 'home_runs')).toBe(true); // other markets survive
});
test('name variants merge (accents/nicknames) — no double entry', () => {
const out = dedupeLeaders([
{ player: 'José Ramírez', stat: 'total_bases', line: 1.5 },
{ player: 'Jose Ramirez', stat: 'total_bases', line: 2.5 },
]);
expect(out).toHaveLength(1);
});
test('reads player_name too (ledger rows) — not just player', () => {
const out = dedupeLeaders([
{ player_name: 'Alec Bohm', stat: 'strikeouts', line: 1.6 },
{ player_name: 'Alec Bohm', stat: 'strikeouts', line: 1.3 },
]);
expect(out).toHaveLength(1);
});
test('empty / malformed → empty, no throw', () => {
expect(dedupeLeaders(null)).toEqual([]);
expect(dedupeLeaders([{}])).toEqual([]);
});
});
describe('groupIntoLadders', () => {
test('nests alt lines under one player+market entry, rungs sorted by line', () => {
const groups = groupIntoLadders([
{ player: 'Alec Bohm', stat: 'strikeouts', line: 1.6, grade: 'B' },
{ player: 'Alec Bohm', stat: 'strikeouts', line: 1.3, grade: 'A' },
{ player: 'Alec Bohm', stat: 'strikeouts', line: 1.5, grade: 'B' },
], (a, b) => (b.grade < a.grade ? b : a)); // pick the best grade as primary
expect(groups).toHaveLength(1);
expect(groups[0].ladder.map((r) => r.line)).toEqual([1.3, 1.5, 1.6]); // rungs ascending
expect(groups[0].primary.grade).toBe('A'); // betterOf picked the A rung
});
test('different markets for the same player stay separate ladders', () => {
const groups = groupIntoLadders([
{ player: 'X', stat: 'hits', line: 0.5 },
{ player: 'X', stat: 'total_bases', line: 1.5 },
]);
expect(groups).toHaveLength(2);
});
});
describe('marketFamily', () => {
test('strips case/space; side+line are not part of the family', () => {
expect(marketFamily('Total Bases')).toBe('total_bases');
expect(marketFamily('stolen_bases')).toBe('stolen_bases');
});
});