Files
vyndr/tests/unit/settleSource.test.js
T
builtbykev b33612675d Heal execute: quarantine markers, re-enable DNP voiding, two exclusion scopes
Order 2 Phases 2 + 4. Pre-heal rollback point secured first:
vyndr-20260720-093821.dump (856,890 bytes) VERIFIED ON THE BOX, not just
exit 0.

MIGRATION 027 — two DISTINCT exclusion scopes, deliberately separate:
- quarantine_reason: the row's GRADE is untrustworthy (wrong_opponent_grade).
  The row REMAINS a real public settled result — the bet happened, the
  outcome is real — but it must never train or validate, so
  getModelAggregate now excludes it from the denominator alongside
  void/unrecoverable.
- analysis_flags: the row is VALID for settlement and the record but
  unattributable for PER-GAME analysis (doubleheader dates). Explicitly NOT
  filtered from aggregates.
Collapsing these would either wrongly drop 166 doubleheader rows from the
record or wrongly keep 25 wrong-opponent grades inside model validation.
Tests assert both directions, including that analysis_flags is NOT filtered.
Also adds re_settled_at + settlement_source to model_snapshots.

DNP VOIDING RE-ENABLED — reversing my own Order 1.5 disable, with scrutiny,
because its premise was FALSE. Order 1.5 assumed a missing player row meant
the row's DATE was wrong. The Phase 0 dry-run disproved it: across every
bindable row the stored date matched a real game (MIS-DATED: 0), and the
players I had cited as counter-evidence were genuine DNPs on their true
dates (Freeman 07-18; Kwan/Hedges/Davis 07-17 — their teams played, they
did not). The evidence is positive: games FINAL + no line in a full-season
log = no bet existed.

I got this wrong twice tonight in opposite directions; the dry-run is what
caught it. Recording the reasoning in the code so the next reader sees why
the flag flipped back.

Suite 282/3386 green, build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
2026-07-20 05:41:41 -04:00

141 lines
5.4 KiB
JavaScript

/**
* Session 64 Order 1 — date-targeted settlement + terminal states.
*
* The bug: a lookup miss did `pending += 1` with no terminal state, so a row
* that could NEVER settle (DNP, or the rolling window passed the game) was
* indistinguishable from one settling tomorrow. These lock the distinctions.
*/
const src = require('../../src/services/settleSource');
const matchesDate = (r, d) => r.date === d;
const statValue = (s, st) => (s && s[st] != null ? Number(s[st]) : null);
const base = {
sport: 'mlb', playerName: 'X', gameDate: '2026-07-17', statType: 'doubles',
matchesDate, statValue,
};
describe('classifyGameState', () => {
test('final states', () => {
expect(src.classifyGameState({ status: 'Final' })).toBe('final');
expect(src.classifyGameState({ state: 'post' })).toBe('final');
});
test('postponed/cancelled are void', () => {
expect(src.classifyGameState({ detailedState: 'Postponed' })).toBe('void');
expect(src.classifyGameState({ status: 'Cancelled' })).toBe('void');
});
test('SUSPENDED is NOT final — it resumes and must stay pending', () => {
expect(src.classifyGameState({ detailedState: 'Suspended' })).toBe('not_final');
});
test('scheduled / in progress are not final', () => {
expect(src.classifyGameState({ status: 'Scheduled' })).toBe('not_final');
expect(src.classifyGameState({ status: 'In Progress' })).toBe('not_final');
});
test('unknown when there is nothing to read', () => {
expect(src.classifyGameState({})).toBe('unknown');
});
});
describe('absenceMeaning — what a missing player row MEANS', () => {
test('any unplayed game → pending (never void a bet that may still settle)', () => {
expect(src.absenceMeaning([{ status: 'Final' }, { status: 'Suspended' }])).toBe('pending');
});
test('all games postponed → void', () => {
expect(src.absenceMeaning([{ status: 'Postponed' }])).toBe('void');
});
test('game final but player absent → void (confirmed DNP)', () => {
expect(src.absenceMeaning([{ status: 'Final' }])).toBe('void');
});
test('no schedule → unknown, not a guess', () => {
expect(src.absenceMeaning([])).toBe('unknown');
});
});
describe('resolveOutcome', () => {
test('player has a line on that date → SETTLED with the real value', async () => {
const r = await src.resolveOutcome({
...base,
getFullLog: async () => [{ date: '2026-07-17', stat: { doubles: 1 } }],
getSchedule: async () => [{ status: 'Final' }],
});
expect(r.state).toBe('settled');
expect(r.value).toBe(1);
});
test('a ZERO value still settles (0 is a real result, not absence)', async () => {
const r = await src.resolveOutcome({
...base,
getFullLog: async () => [{ date: '2026-07-17', stat: { doubles: 0 } }],
getSchedule: async () => [{ status: 'Final' }],
});
expect(r.state).toBe('settled');
expect(r.value).toBe(0);
});
// Session 64 — this MUST NOT void. Player-absence from a labelled date can
// equally mean the row's date is wrong (ledger derives game_date from the
// GRADE time when the feed has no game_time, so late-UTC snapshots label the
// previous ET day). Verified live: Freddie Freeman's 97-game log has no
// 2026-07-18 because he played the 17th and 19th. Voiding on absence
// destroyed real results.
test('player absent on a FINAL game → VOID (dates proven correct; DNP is real)', async () => {
const r = await src.resolveOutcome({
...base,
getFullLog: async () => [{ date: '2026-07-16', stat: { doubles: 0 } }],
getSchedule: async () => [{ status: 'Final' }],
});
expect(r.state).toBe('void');
expect(r.reason).toBe('player_dnp');
});
test('postponed game → VOID with the postponed reason', async () => {
const r = await src.resolveOutcome({
...base,
getFullLog: async () => [],
getSchedule: async () => [{ status: 'Postponed' }],
});
expect(r.state).toBe('void');
expect(r.reason).toBe('game_postponed_or_cancelled');
});
test('SUSPENDED game stays PENDING — never voided', async () => {
const r = await src.resolveOutcome({
...base,
getFullLog: async () => [],
getSchedule: async () => [{ status: 'Suspended' }],
});
expect(r.state).toBe('pending');
});
test('a game OUTSIDE a rolling window still settles from the full log', async () => {
// The exact live failure: Jul 12 game, window now starts Jul 6.
const full = Array.from({ length: 40 }, (_, i) => ({
date: `2026-07-${String(i + 1).padStart(2, '0')}`, stat: { doubles: i % 3 },
}));
const r = await src.resolveOutcome({
...base, gameDate: '2026-07-12',
getFullLog: async () => full,
getSchedule: async () => [{ status: 'Final' }],
});
expect(r.state).toBe('settled');
});
test('player PLAYED but the stat is missing from the box row → unknown, NEVER void', async () => {
const r = await src.resolveOutcome({
...base,
getFullLog: async () => [{ date: '2026-07-17', stat: { hits: 2 } }],
getSchedule: async () => [{ status: 'Final' }],
});
expect(r.state).toBe('unknown');
expect(r.reason).toBe('stat_not_in_box_row');
});
test('a throwing source degrades to unknown, never throws', async () => {
const r = await src.resolveOutcome({
...base,
getFullLog: async () => { throw new Error('net'); },
getSchedule: async () => { throw new Error('net'); },
});
expect(r.state).toBe('unknown');
});
});