Files
vyndr/tests/unit/propViability.test.js
builtbykev 02c17a65c3 S5 (a1): prop viability — lineups, injury wire, date navigation
- lineupService: statsapi hydrate=lineups (live shape verified) →
  CONFIRMED (batting slot) / NOT_IN (team posted without the player) /
  PROJECTED (not posted). 10-min cache, pure parser, injectable.
- NOT_IN visibly KILLS the grade on the slate: struck through + NOT IN
  LINEUP chip, parlay/book actions suppressed. The locked ledger read is
  untouched — honesty is showing the read is dead, not deleting it.
- injuryService: ESPN injuries feed → OUT/GTD/PROB chips (unknown status
  → no chip, never invented). Chips on slate strips via ViabilityChips.
- Date navigation on the Slate: YESTERDAY (results surface — finals +
  THE SETTLE panel of that date's settled reads w/ outcome + CLV chips,
  via new ?date= filter on /api/ledger/model) / TODAY / TOMORROW
  (schedule until lines post). Odds/grades/pitcher layers are TODAY's
  and never fake other dates; 60s poll only refreshes today.
- Routes /api/schedule/:sport/lineups + /injuries + Next proxies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:38:37 -04:00

102 lines
4.3 KiB
JavaScript

// Session 64 (A1-S5) — prop viability: lineup confirmation + injury wire +
// the NOT-IN grade kill. Real feeds only; absent → no chips, never guessed.
const { parseLineups, statusFor } = require('../../src/services/lineupService');
const { parseInjuries, chipFor } = require('../../src/services/injuryService');
const adapter = require('../../web/src/lib/slateAdapter');
// Real statsapi hydrate=lineups shape (verified live 2026-07-11).
const SCHEDULE_JSON = {
dates: [{
games: [
{
gamePk: 823357,
teams: { home: { team: { name: 'Milwaukee Brewers' } }, away: { team: { name: 'Pittsburgh Pirates' } } },
lineups: {
homePlayers: [
{ id: 663968, fullName: 'Jake Mangum' },
{ id: 664040, fullName: 'Brandon Lowe' },
],
awayPlayers: [{ id: 1, fullName: 'Bryan Reynolds' }],
},
},
{ gamePk: 823356, teams: { home: { team: { name: 'Detroit Tigers' } }, away: { team: { name: 'Tampa Bay Rays' } } }, lineups: {} },
],
}],
};
describe('lineupService — CONFIRMED / NOT_IN / PROJECTED', () => {
const parsed = parseLineups(SCHEDULE_JSON);
test('posted lineup → confirmed with the batting slot', () => {
expect(parsed.byPlayer['brandon lowe']).toEqual({ status: 'confirmed', slot: 2, team: 'Milwaukee Brewers' });
expect(parsed.postedTeams).toContain('Milwaukee Brewers');
});
test('team posted, player absent → NOT_IN', () => {
expect(statusFor('Christian Yelich', 'Milwaukee Brewers', parsed).status).toBe('not_in');
});
test('team not posted → PROJECTED (never guessed dead)', () => {
expect(statusFor('Riley Greene', 'Detroit Tigers', parsed).status).toBe('projected');
});
test('accented/variant names resolve through nameKey', () => {
expect(statusFor('Brandon Lowé', 'Milwaukee Brewers', parsed).status).toBe('confirmed');
});
});
describe('injuryService — OUT / GTD / PROB chips', () => {
test('parses the ESPN feed shape', () => {
const byPlayer = parseInjuries({
injuries: [{
displayName: 'Arizona Diamondbacks',
injuries: [
{ athlete: { displayName: 'Ketel Marte' }, status: 'Out', details: { type: 'Hamstring' } },
{ athlete: { displayName: 'Corbin Carroll' }, status: 'Day-To-Day', shortComment: 'wrist' },
],
}],
});
expect(byPlayer['ketel marte']).toMatchObject({ status: 'OUT', detail: 'Hamstring' });
expect(byPlayer['corbin carroll'].status).toBe('GTD');
});
test('unknown status text → no chip (never invented)', () => {
expect(chipFor('Active')).toBeNull();
expect(chipFor('')).toBeNull();
});
});
describe('slateAdapter — NOT_IN kills the graded props on the strip', () => {
const GAME = { home: 'Milwaukee Brewers', away: 'Pittsburgh Pirates' };
const props = [{ player: 'Christian Yelich', stat_type: 'hits', line: 0.5 }];
const gradeIndex = adapter.indexGrades([
{ player: 'Christian Yelich', stat_type: 'hits', line: 0.5, direction: 'over', grade: 'A',
team: 'Milwaukee Brewers', gradedAt: { line: 0.5, timestamp: 'x' } },
]);
// Fixture keys computed via nameKey — the same folding BOTH real sides use
// (hand-written keys miss nickname resolution, e.g. jake→jacob).
const { nameKey } = require('../../web/src/lib/playerName');
const viability = {
lineups: { byPlayer: { [nameKey('Jake Mangum')]: { status: 'confirmed', slot: 1 } }, postedTeams: ['Milwaukee Brewers'] },
injuries: {},
};
test('graded prop for a NOT_IN player renders dead (grade preserved)', () => {
const strips = adapter.buildPlayerStripsFromProps(props, gradeIndex, {}, Date.now(), GAME, viability);
expect(strips[0].lineup.status).toBe('not_in');
expect(strips[0].props[0].dead).toBe(true);
expect(strips[0].props[0].grade).toBe('A'); // struck through in UI, never deleted
});
test('confirmed player carries the slot; no viability feed → no chips', () => {
const confirmed = adapter.buildPlayerStripsFromProps(
[{ player: 'Jake Mangum', stat_type: 'hits', line: 0.5 }],
{}, {}, Date.now(), GAME, viability,
);
expect(confirmed[0].lineup).toEqual({ status: 'confirmed', slot: 1 });
const none = adapter.buildPlayerStripsFromProps(props, gradeIndex, {}, Date.now(), GAME, null);
expect(none[0].lineup == null).toBe(true);
expect(none[0].props[0].dead == null).toBe(true);
});
});