Files
vyndr/tests/unit/eventPublication.test.js
T
builtbykev 8c6aef1e12 Event admission gate: an unresolved or contradicted game does not earn a Read
A fractured identity keeps two bad records from merging. It does not make an
unknown game true. Until now a prop with an unresolved or verified-impossible
event still continued into grading under that synthetic key; it no longer does.

- event_binding_status contract: RESOLVED / UNRESOLVED / AMBIGUOUS /
  CONTRADICTED / UNSUPPORTED. CONTRADICTED and UNRESOLVED stay distinct — one
  means we know the association is wrong, the other that we do not know.
- ONE admission gate (gradeSlateService.admitForGrading), before dedupe and
  before grading. Admission requires RESOLVED *and* a canonical_event_id; a
  legacy derived game_id can never satisfy it. Rejected props are returned, not
  discarded, so retention keeps them as evidence.
- Roster evidence is now DATE-SCOPED. statsapi honours ?date= and it changes the
  answer (Joe Mack is on the 2026-08-26 Marlins roster, absent on 2026-04-15).
  Evidence that does not describe the slate's date can only yield UNRESOLVED,
  never CONTRADICTED — uncertainty must not become an accusation.
- A mis-nested market is REFUSED, never re-bound. Knowing Joe Mack is a Marlin
  does not license moving a provider record into the Marlins game; that would
  invent provenance.

Measured on the real 2026-08-26 slate: 2,878 props -> 2,874 admitted, 4 rejected
(EVENT_PLAYER_TEAM_CONTRADICTION), each player's correct game still resolving.
Doubleheader 824514/824478 both remain independently RESOLVED and admitted.

No change to probability, projection, side, grade, confidence, ranking,
normalization or calibration. Lineage remains disabled.

Suite 380/5,061/0 from the release worktree; web tsc exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
2026-08-27 16:50:50 -04:00

925 lines
44 KiB
JavaScript

'use strict';
/**
* CANONICAL EVENT IDENTITY + PUBLICATION COMMIT TRUTH.
*
* Two findings drive this suite, both verified against production:
*
* 1. EVENT LABELS ARE NOT EVENT IDENTITY. `gameIdFor` derives
* `sport:date:away@home`, which has no occurrence component, so both halves
* of a doubleheader produce a byte-identical string. MLB 2026 through
* 2026-08-27: 19 doubleheaders / 38 real games collapse into 19 labels. The
* 2026-08-17 St. Louis @ Cincinnati pair (gamePk 824514 / 824478) landed in
* ONE id holding 171 ledger rows and FOUR distinct starting pitchers.
*
* 2. PUBLISHED MUST MEAN COMMITTED. Retention persisted at
* snapshotService:1137 while the authoritative slate write is at :1152, and
* the winner was marked published earlier still.
*/
const path = require('path');
const fs = require('fs');
const E = require('../../src/services/event/eventIdentity');
const L = require('../../src/services/read/readLineage');
const retention = require('../../src/services/retentionService');
const ROOT = path.resolve(__dirname, '..', '..');
// The REAL doubleheader, from statsapi.
const DH_GAMES = [
{ gamePk: 824514, gameNumber: 1, gameDate: '2026-08-17T17:40:00Z', home: { team: 'Cincinnati Reds' }, away: { team: 'St. Louis Cardinals' } },
{ gamePk: 824478, gameNumber: 2, gameDate: '2026-08-17T22:40:00Z', home: { team: 'Cincinnati Reds' }, away: { team: 'St. Louis Cardinals' } },
];
const dhProp = (time, over = {}) => ({
player: 'Andre Pallante', stat_type: 'strikeouts', line: 4.5, book: 'draftkings',
home_team: 'Cincinnati Reds', away_team: 'St. Louis Cardinals',
game_date: '2026-08-17', game_time: time, ...over,
});
describe('EVENT IDENTITY — canonical, sport-neutral contract', () => {
test('MLB resolves to a namespaced canonical id from statsapi gamePk', () => {
const r = E.resolveEvent('mlb', dhProp('2026-08-17T17:40:00Z'), DH_GAMES);
expect(r.canonical_event_id).toBe('mlb:gamepk:824514');
expect(r.event_identity_source).toBe(E.IDENTITY_SOURCE.MLB_STATSAPI_GAMEPK);
expect(r.event_identity_method).toBe(E.IDENTITY_METHOD.CANONICAL);
expect(r.event_occurrence).toBe(1);
});
test('an unsupported sport records UNSUPPORTED_SPORT, never a guessed id', () => {
for (const sp of ['wnba', 'nba', 'soccer', 'nfl']) {
const r = E.resolveEvent(sp, dhProp('2026-08-17T17:40:00Z'), DH_GAMES);
expect(r.event_identity_method).toBe(E.IDENTITY_METHOD.UNSUPPORTED_SPORT);
expect(r.canonical_event_id).toBeNull();
}
});
test('team resolution is canonical, handling every real spelling', () => {
const cases = [
['Cincinnati Reds', 'CIN'], ['CINReds', 'CIN'], ['CIN', 'CIN'],
['St. Louis Cardinals', 'STL'], ['St.LouisCardinals', 'STL'],
['New York Mets', 'NYM'], ['NYMets', 'NYM'], ['CLEGuardians', 'CLE'],
// The trap: both end in "sox". Longest nickname must win.
['Chicago White Sox', 'CWS'], ['ChicagoWhiteSox', 'CWS'],
['Boston Red Sox', 'BOS'], ['BOSRedSox', 'BOS'],
['Toronto Blue Jays', 'TOR'], ['TORBlueJays', 'TOR'],
];
for (const [name, abbr] of cases) expect(E.teamAbbr(name)).toBe(abbr);
expect(E.teamAbbr('Not A Team')).toBeNull();
expect(E.teamAbbr('')).toBeNull();
});
test('an unresolvable team name matches NOTHING rather than falling back to text', () => {
expect(E.teamsMatch(DH_GAMES[0], 'Cincinnati Reds', 'Some Fake Club')).toBe(false);
});
});
describe('DOUBLEHEADER SAFETY — the mandatory proof', () => {
test('two games of one doubleheader receive DIFFERENT canonical ids', () => {
const g1 = E.resolveEvent('mlb', dhProp('2026-08-17T17:40:00Z'), DH_GAMES);
const g2 = E.resolveEvent('mlb', dhProp('2026-08-17T22:40:00Z'), DH_GAMES);
expect(g1.canonical_event_id).toBe('mlb:gamepk:824514');
expect(g2.canonical_event_id).toBe('mlb:gamepk:824478');
expect(g1.canonical_event_id).not.toBe(g2.canonical_event_id);
expect(g1.event_occurrence).toBe(1);
expect(g2.event_occurrence).toBe(2);
});
test('both survive dedupe — same player, stat and line, different games', () => {
const gs = require('../../src/services/gradeSlateService');
const props = [dhProp('2026-08-17T17:40:00Z'), dhProp('2026-08-17T22:40:00Z')];
E.attachEventIdentity('mlb', props, DH_GAMES);
const kept = gs.__internals
? gs.__internals.dedupeProps(props, 100)
: null;
expect(kept).not.toBeNull();
expect(kept).toHaveLength(2);
expect(new Set(kept.map((p) => p.canonical_event_id)).size).toBe(2);
});
test('their LEGACY derived label collides, and that no longer matters', () => {
const gameIdFor = require('../../src/services/ledgerService').__internals.gameIdFor;
const prop = { away_team: 'St. Louis Cardinals', home_team: 'Cincinnati Reds' };
const a = gameIdFor('mlb', prop, '2026-08-17');
const b = gameIdFor('mlb', prop, '2026-08-17');
expect(a).toBe(b); // the label cannot separate them...
// ...but canonical identity does, and lineage keys on that.
const base = {
sport: 'mlb', game_date: '2026-08-17', player_key: 'andre pallante',
stat: 'strikeouts', side: 'over', line: 4.5, game_id: a,
captured_at: '2026-08-17T14:00:00Z', published: true, grade: 'B', p_win: 0.6,
};
const r1 = { ...base, canonical_event_id: 'mlb:gamepk:824514' };
const r2 = { ...base, canonical_event_id: 'mlb:gamepk:824478' };
expect(L.readNaturalKey(r1)).not.toBe(L.readNaturalKey(r2));
});
test('neither game can supersede or overwrite the other', () => {
const base = {
sport: 'mlb', game_date: '2026-08-17', player_key: 'andre pallante',
stat: 'strikeouts', side: 'over', line: 4.5, game_id: 'mlb:2026-08-17:StL@Cin',
captured_at: '2026-08-17T14:00:00Z', published: true, grade: 'B', p_win: 0.6,
};
const g1 = { ...base, canonical_event_id: 'mlb:gamepk:824514' };
const g2 = { ...base, canonical_event_id: 'mlb:gamepk:824478' };
const r1 = L.resolveLineage({ candidate: g1, existing: [], mintReadId: () => 'RA' });
const row1 = {
id: 101, read_id: 'RA', read_natural_key: r1.read_natural_key, game_id: base.game_id,
claim_digest: r1.claim_digest, revision_ordinal: 0, lineage_action: 'ORIGIN',
supersedes_id: null, captured_at: base.captured_at, claim: g1,
};
const r2 = L.resolveLineage({ candidate: g2, existing: [row1], mintReadId: () => 'RB' });
expect(r2.read_id).toBe('RB');
expect(r2.action).toBe(L.LINEAGE_ACTION.ORIGIN);
expect(r2.supersedes_id).toBeNull();
});
test('identity is decided by TIME, and refuses when time cannot decide', () => {
const noTime = E.resolveEvent('mlb', dhProp(null), DH_GAMES);
expect(noTime.event_identity_method).toBe(E.IDENTITY_METHOD.UNRESOLVED);
expect(noTime.reason).toBe('ambiguous_no_game_time');
// A time matching neither game is refused rather than snapped to the nearer.
const farOff = E.resolveEvent('mlb', dhProp('2026-08-17T05:00:00Z'), DH_GAMES);
expect(farOff.event_identity_method).toBe(E.IDENTITY_METHOD.UNRESOLVED);
expect(farOff.reason).toBe('ambiguous_time_too_far');
});
test('a single-game day needs no time to resolve', () => {
const r = E.resolveEvent('mlb', dhProp(null), [DH_GAMES[0]]);
expect(r.canonical_event_id).toBe('mlb:gamepk:824514');
});
});
describe('DEDUPE SEMANTICS — collapse books, never events', () => {
const gs = require('../../src/services/gradeSlateService');
const dedupe = (props, n) => gs.__internals.dedupeProps(props, n);
test('the same proposition from several books still collapses to one', () => {
const props = ['draftkings', 'fanduel', 'betmgm'].map((book) =>
({ ...dhProp('2026-08-17T17:40:00Z'), book, canonical_event_id: 'mlb:gamepk:824514' }));
expect(dedupe(props, 100)).toHaveLength(1);
});
test('the same proposition in two events does NOT collapse', () => {
const props = [
{ ...dhProp('2026-08-17T17:40:00Z'), canonical_event_id: 'mlb:gamepk:824514' },
{ ...dhProp('2026-08-17T22:40:00Z'), canonical_event_id: 'mlb:gamepk:824478' },
];
expect(dedupe(props, 100)).toHaveLength(2);
});
test('without canonical identity behaviour is UNCHANGED from before', () => {
// Same teams, same date, no canonical id: the legacy fallback collapses
// them exactly as the old key did. The fix does not pretend to distinguish
// what it cannot.
const props = [dhProp('2026-08-17T17:40:00Z'), dhProp('2026-08-17T22:40:00Z')];
expect(dedupe(props, 100)).toHaveLength(1);
});
test('different games on the same date never collapse even without canonical id', () => {
const a = { ...dhProp('t'), home_team: 'Cincinnati Reds', away_team: 'St. Louis Cardinals' };
const b = { ...dhProp('t'), home_team: 'Boston Red Sox', away_team: 'Arizona Diamondbacks' };
expect(dedupe([a, b], 100)).toHaveLength(2);
});
});
describe('PUBLICATION COMMIT TRUTH', () => {
const served = () => ({
sport: 'mlb', game_date: '2026-08-17', player_key: 'x', stat: 'hits', side: 'over',
line: 0.5, game_id: 'g', canonical_event_id: 'mlb:gamepk:824514',
captured_at: '2026-08-17T14:00:00Z', grade: 'B', p_win: 0.61, published: true,
});
const okClient = () => ({ from: () => ({ upsert: async () => ({ error: null }) }) });
test('capture does NOT resolve lineage — persist leaves it null', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/services/retentionService.js'), 'utf8');
const persistBody = src.slice(src.indexOf('async function persist('));
expect(persistBody).not.toMatch(/attachLineage\(/);
});
test('the slate write PRECEDES the publication commit', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/services/snapshotService.js'), 'utf8');
const persistAt = src.indexOf('await persistRetention(enriched');
const slateAt = src.indexOf('cacheSet(`snapshot:${sp}:latest`');
const commitAt = src.indexOf('commitPublication({');
expect(persistAt).toBeGreaterThan(-1);
expect(slateAt).toBeGreaterThan(persistAt);
expect(commitAt).toBeGreaterThan(slateAt);
});
test('WINNER SELECTED + REDIS OK -> the exact snapshot becomes published', async () => {
const rows = [served()];
const out = await retention.commitPublication(
{ rows, publicationId: 'slate-1', publishedAt: '2026-08-17T15:00:00Z' },
{ getClient: okClient, fetchExisting: async () => [], mintReadId: () => 'R1' },
);
expect(out.published).toBe(1);
expect(rows[0].published_at).toBe('2026-08-17T15:00:00Z');
expect(rows[0].publication_id).toBe('slate-1');
expect(rows[0].lineage_action).toBe('ORIGIN');
});
test('WINNER SELECTED + REDIS FAILS -> nothing is published', () => {
// The commit is unreachable when the slate write throws: it sits after the
// awaited cacheSet in the same function, with no catch between them.
const src = fs.readFileSync(path.join(ROOT, 'src/services/snapshotService.js'), 'utf8');
const slateAt = src.indexOf('cacheSet(`snapshot:${sp}:latest`');
const commitAt = src.indexOf('commitPublication({');
const between = src.slice(slateAt, commitAt);
expect(between).not.toMatch(/catch\s*\(/);
});
test('a row that was never served is not committed even in a served batch', async () => {
const rows = [served(), { ...served(), side: 'under', published: false }];
const out = await retention.commitPublication(
{ rows, publicationId: 'slate-1', publishedAt: 't' },
{ getClient: okClient, fetchExisting: async () => [], mintReadId: () => 'R1' },
);
expect(out.candidates).toBe(1);
expect(rows[1].published_at).toBeUndefined();
expect(rows[1].lineage_action).toBeUndefined();
});
test('REDIS OK + PUBLICATION UPDATE FAILS -> understated, never overstated', async () => {
const rows = [served()];
const failing = () => ({ from: () => ({ upsert: async () => ({ error: { message: 'db down' } }) }) });
const out = await retention.commitPublication(
{ rows, publicationId: 'slate-1', publishedAt: 't' },
{ getClient: failing, fetchExisting: async () => [], mintReadId: () => 'R1' },
);
expect(out.published).toBe(0);
expect(out.error).toBe('db down');
});
test('no database configured skips without throwing', async () => {
const out = await retention.commitPublication(
{ rows: [served()], publicationId: 's', publishedAt: 't' },
{ getClient: () => null },
);
expect(out.skipped).toBe(true);
expect(out.error).toBeNull();
});
test('retrying the commit does not manufacture a second publication', async () => {
const rows = [served()];
const deps = { getClient: okClient, fetchExisting: async () => [], mintReadId: () => 'R1' };
await retention.commitPublication({ rows, publicationId: 's', publishedAt: 't' }, deps);
const first = rows[0].claim_digest;
// Second run sees the first as existing history.
const existing = [{
id: 1, read_id: rows[0].read_id, read_natural_key: rows[0].read_natural_key,
game_id: rows[0].game_id, claim_digest: first, revision_ordinal: 0,
lineage_action: 'ORIGIN', supersedes_id: null, captured_at: rows[0].captured_at,
claim: rows[0],
}];
const rows2 = [served()];
const out2 = await retention.commitPublication(
{ rows: rows2, publicationId: 's', publishedAt: 't' },
{ ...deps, fetchExisting: async () => existing },
);
expect(out2.lineage.recaptures).toBe(1);
expect(out2.lineage.revisions).toBe(0);
});
});
describe('PUBLICATION CLOCK', () => {
test('captured_at and published_at are different fields answering different questions', async () => {
const rows = [{
sport: 'mlb', game_date: '2026-08-17', player_key: 'x', stat: 'hits', side: 'over',
line: 0.5, game_id: 'g', canonical_event_id: 'mlb:gamepk:824514',
captured_at: '2026-08-17T14:00:00Z', grade: 'B', p_win: 0.61, published: true,
}];
await retention.commitPublication(
{ rows, publicationId: 's', publishedAt: '2026-08-17T15:07:00Z' },
{ getClient: () => ({ from: () => ({ upsert: async () => ({ error: null }) }) }), fetchExisting: async () => [], mintReadId: () => 'R1' },
);
expect(rows[0].captured_at).toBe('2026-08-17T14:00:00Z');
expect(rows[0].published_at).toBe('2026-08-17T15:07:00Z');
expect(rows[0].published_at).not.toBe(rows[0].captured_at);
});
});
describe('CONCURRENCY RECOVERY — bounded', () => {
const served = () => ({
sport: 'mlb', game_date: '2026-08-17', player_key: 'x', stat: 'hits', side: 'over',
line: 0.5, game_id: 'g', canonical_event_id: 'mlb:gamepk:824514',
captured_at: 't', grade: 'B', p_win: 0.61, published: true,
});
const conflict = { message: 'duplicate key value violates unique constraint "model_snapshots_supersedes_unique"' };
test('the supersedes conflict is recognised and other errors are not', () => {
expect(retention.isSupersedesConflict(conflict)).toBe(true);
expect(retention.isSupersedesConflict({ message: 'connection refused' })).toBe(false);
expect(retention.isSupersedesConflict(null)).toBe(false);
});
test('a losing writer re-resolves against the new head and succeeds', async () => {
let n = 0;
const sb = { from: () => ({ upsert: async () => { n += 1; return n === 1 ? { error: conflict } : { error: null }; } }) };
const out = await retention.commitPublication(
{ rows: [served()], publicationId: 's', publishedAt: 't' },
{ getClient: () => sb, fetchExisting: async () => [], mintReadId: () => 'R1' },
);
expect(out.conflicts).toBe(1);
expect(out.recovered).toBe(1);
expect(out.published).toBe(1);
});
test('a permanent conflict fails EXPLICITLY and is bounded', async () => {
let n = 0;
const sb = { from: () => ({ upsert: async () => { n += 1; return { error: conflict }; } }) };
const out = await retention.commitPublication(
{ rows: [served()], publicationId: 's', publishedAt: 't' },
{ getClient: () => sb, fetchExisting: async () => [], mintReadId: () => 'R1' },
);
expect(out.recovery_failed).toBe(1);
expect(out.error).toBeTruthy();
// 1 initial + FORK_RETRY_LIMIT attempts. Bounded, never a spin.
expect(n).toBe(1 + retention.FORK_RETRY_LIMIT);
expect(retention.FORK_RETRY_LIMIT).toBeLessThanOrEqual(3);
});
test('a non-conflict error is NOT retried', async () => {
let n = 0;
const sb = { from: () => ({ upsert: async () => { n += 1; return { error: { message: 'db down' } }; } }) };
const out = await retention.commitPublication(
{ rows: [served()], publicationId: 's', publishedAt: 't' },
{ getClient: () => sb, fetchExisting: async () => [], mintReadId: () => 'R1' },
);
expect(n).toBe(1);
expect(out.error).toBe('db down');
expect(out.conflicts).toBeUndefined();
});
});
describe('CHANGE SEMANTICS — identity is not an opinion', () => {
test('correcting a derived event label is NOT a BELIEF change', () => {
const a = { line: 0.5, side: 'over', p_win: 0.61, grade: 'B', canonical_event_id: null };
const b = { ...a, canonical_event_id: 'mlb:gamepk:824514' };
expect(L.CLAIM_FIELD_CLASSES.canonical_event_id).toBeUndefined();
expect(L.classifyChange(a, b).change_type).toBe(L.CHANGE_TYPE.NO_MATERIAL_PUBLISHED_CHANGE);
expect(L.classifyChange(a, b).belief_changed).toBe(false);
});
});
describe('SETTLEMENT SEPARATION still holds after propagation', () => {
test('canonical event, publication and lineage fields are claim-owned', () => {
for (const f of ['published', 'claim_digest', 'revision_ordinal', 'change_type']) {
expect(L.settlementUpdateIsSafe({ outcome: 'hit', [f]: 'x' }).violations).toContain(f);
}
});
test('the real production settle patch still passes', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/services/snapshotSettlementService.js'), 'utf8');
const m = src.match(/\.update\(\{([\s\S]*?)\}\)/);
const keys = [...m[1].matchAll(/^\s*([a-z_0-9]+)\s*:/gm)].map((x) => x[1]);
expect(L.settlementUpdateIsSafe(Object.fromEntries(keys.map((k) => [k, 1])))).toMatchObject({ safe: true });
});
});
describe('PRODUCTION PRESERVATION', () => {
test('no product path imports event identity or lineage', () => {
const walk = (dir, out = []) => {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
const full = path.join(dir, e.name);
if (e.isDirectory()) walk(full, out);
else if (/\.(js|jsx|ts|tsx)$/.test(e.name)) out.push(full);
}
return out;
};
const allowed = new Set([
path.join(ROOT, 'src/services/read/readLineage.js'),
path.join(ROOT, 'src/services/retentionService.js'),
path.join(ROOT, 'src/services/event/eventIdentity.js'),
path.join(ROOT, 'src/services/snapshotService.js'),
]);
const files = [
...walk(path.join(ROOT, 'src/routes')),
...(fs.existsSync(path.join(ROOT, 'web/src')) ? walk(path.join(ROOT, 'web/src')) : []),
].filter((f) => !allowed.has(f));
const offenders = files.filter((f) => {
const src = fs.readFileSync(f, 'utf8')
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1');
return /(require\(|from\s+)['"`][^'"`]*(readLineage|eventIdentity)['"`]/.test(src);
}).map((f) => path.relative(ROOT, f));
expect(offenders).toEqual([]);
});
test('the served envelope still comes from the cache', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/routes/snapshot.js'), 'utf8');
expect(src).toMatch(/cacheGet/);
expect(src).not.toMatch(/model_snapshots/);
expect(src).not.toMatch(/canonical_event_id/);
});
});
describe('CANARY SCOPE — bounded and reversible', () => {
const load = (val) => {
const prev = process.env.LINEAGE_CANARY_SPORTS;
if (val === undefined) delete process.env.LINEAGE_CANARY_SPORTS;
else process.env.LINEAGE_CANARY_SPORTS = val;
jest.resetModules();
// eslint-disable-next-line global-require
const m = require('../../src/services/snapshotService');
if (prev === undefined) delete process.env.LINEAGE_CANARY_SPORTS;
else process.env.LINEAGE_CANARY_SPORTS = prev;
return m;
};
test('DARK BY DEFAULT — an unset flag enables lineage for nothing', () => {
// This release deploys the integrity repairs with the observer off.
const m = load(undefined);
expect(m.LINEAGE_CANARY_SPORTS).toEqual([]);
for (const sp of ['mlb', 'wnba', 'nba', 'soccer']) {
expect(m.lineageCanaryEnabled(sp)).toBe(false);
}
});
test('MLB is enabled only by explicit configuration', () => {
const m = load('mlb');
expect(m.lineageCanaryEnabled('mlb')).toBe(true);
for (const sp of ['wnba', 'nba', 'soccer']) {
expect(m.lineageCanaryEnabled(sp)).toBe(false);
}
});
test('an empty value is a kill switch needing no migration revert', () => {
const m = load('');
expect(m.LINEAGE_CANARY_SPORTS).toEqual([]);
expect(m.lineageCanaryEnabled('mlb')).toBe(false);
});
test('a sport without a verified event resolver cannot be canaried into safety', () => {
// Widening the flag does NOT create identity: eventIdentity still refuses,
// so a widened sport records UNSUPPORTED_SPORT and its lineage stays honest.
const m = load('mlb,wnba');
expect(m.lineageCanaryEnabled('wnba')).toBe(true);
const r = E.resolveEvent('wnba', dhProp('2026-08-17T17:40:00Z'), DH_GAMES);
expect(r.canonical_event_id).toBeNull();
expect(r.event_identity_method).toBe(E.IDENTITY_METHOD.UNSUPPORTED_SPORT);
});
test('the commit is gated on the flag in the live path', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/services/snapshotService.js'), 'utf8');
expect(src).toMatch(/commitPublication && persistedRows && lineageCanaryEnabled\(sp\)/);
});
});
describe('UNRESOLVED EVENT SAFETY — may lose coverage, may never merge', () => {
const gs = require('../../src/services/gradeSlateService');
const dedupe = (props, n) => gs.__internals.dedupeProps(props, n);
const base = (o) => ({
player: 'Andre Pallante', stat_type: 'strikeouts', line: 4.5, book: 'draftkings',
home_team: 'Cincinnati Reds', away_team: 'St. Louis Cardinals',
game_date: '2026-08-17', ...o,
});
test('an UNRESOLVED MLB prop never merges with another — it fractures', () => {
// Two rows that could be two different games. Merging them would be the
// exact defect this change removes, so they must both survive.
const props = [
base({ event_identity_method: 'UNRESOLVED' }),
base({ event_identity_method: 'UNRESOLVED', book: 'fanduel' }),
];
expect(dedupe(props, 100)).toHaveLength(2);
});
test('the unresolved key is NOT date+teams and NOT the legacy label', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/services/gradeSlateService.js'), 'utf8');
const fn = src.slice(src.indexOf('function propositionEventKey'), src.indexOf('function dedupeProps'));
// The legacy label is reachable ONLY for sports with no resolver.
expect(fn).toMatch(/expectsCanonical/);
expect(fn).toMatch(/unresolved-event:/);
// And the fracture branch is returned BEFORE the legacy label is built.
expect(fn.indexOf('unresolved-event:')).toBeLessThan(fn.indexOf('legacy:'));
});
test('a resolved event still collapses books — the real dedupe job survives', () => {
const props = ['draftkings', 'fanduel', 'betmgm'].map((book) =>
base({ book, canonical_event_id: 'mlb:gamepk:824514' }));
expect(dedupe(props, 100)).toHaveLength(1);
});
test('a sport with NO resolver keeps legacy behaviour, unchanged', () => {
// WNBA never had canonical identity; fracturing it would multiply grading
// load for no safety gain, and its lineage is excluded from the canary.
const props = [
base({ event_identity_method: 'UNSUPPORTED_SPORT' }),
base({ event_identity_method: 'UNSUPPORTED_SPORT', book: 'fanduel' }),
];
expect(dedupe(props, 100)).toHaveLength(1);
});
test('unresolved MLB props from two REAL games cannot collapse', () => {
// Same player/stat/line/teams/date, identity unknown for both.
const g1 = base({ event_identity_method: 'UNRESOLVED', game_time: '2026-08-17T17:40:00Z' });
const g2 = base({ event_identity_method: 'UNRESOLVED', game_time: '2026-08-17T22:40:00Z' });
expect(dedupe([g1, g2], 100)).toHaveLength(2);
});
});
describe('EXACT PARITY-GAP IDENTITY', () => {
const served = () => ({
sport: 'mlb', snapshot_id: 'snap-1', game_date: '2026-08-17', player_key: 'judge',
stat: 'hits', side: 'over', line: 0.5, game_id: 'g',
canonical_event_id: 'mlb:gamepk:824514', captured_at: '2026-08-17T14:00:00Z',
grade: 'B', p_win: 0.61, published: true,
});
test('a failed commit names WHICH served claim is missing', async () => {
const failing = () => ({ from: () => ({ upsert: async () => ({ error: { message: 'db down' } }) }) });
const out = await retention.commitPublication(
{ rows: [served()], publicationId: 'slate-9', publishedAt: '2026-08-17T15:00:00Z' },
{ getClient: failing, fetchExisting: async () => [], mintReadId: () => 'R1', now: () => 't5' },
);
expect(out.parity_gap).toBeTruthy();
expect(out.parity_gap.reason).toBe('db down');
expect(out.parity_gap.missing_count).toBe(1);
const m = out.parity_gap.missing[0];
// Deterministic recovery, not "lineage failed".
for (const k of ['snapshot_id', 'player_key', 'stat', 'line', 'side', 'canonical_event_id', 'captured_at']) {
expect(m[k]).not.toBeNull();
}
expect(out.parity_gap.publication_id).toBe('slate-9');
expect(out.parity_gap.published_at).toBe('2026-08-17T15:00:00Z');
expect(out.parity_gap.attempted_at).toBe('t5');
});
test('a clean commit records NO parity gap', async () => {
const ok = () => ({ from: () => ({ upsert: async () => ({ error: null }) }) });
const out = await retention.commitPublication(
{ rows: [served()], publicationId: 's', publishedAt: 't' },
{ getClient: ok, fetchExisting: async () => [], mintReadId: () => 'R1' },
);
expect(out.parity_gap).toBeUndefined();
expect(out.published).toBe(1);
});
});
describe('PROPAGATION — canonical identity survives every hop', () => {
test('the graded base carries identity from the prop', () => {
// Source assertion on the ONE place the prop→base copy happens. A test that
// only built props with the field already set could not catch it being
// dropped here, which is exactly what the teeth run exposed.
const src = fs.readFileSync(path.join(ROOT, 'src/services/gradeSlateService.js'), 'utf8');
const base = src.slice(src.indexOf('const base = {'), src.indexOf('const sides = await'));
expect(base).toMatch(/canonical_event_id:\s*prop\.canonical_event_id/);
expect(base).toMatch(/event_identity_method:\s*prop\.event_identity_method/);
expect(base).not.toMatch(/canonical_event_id:\s*null\s*,/);
});
test('the retention row carries identity from the graded base', () => {
const c = retention.createCollector({
snapshotId: 's1', capturedAt: '2026-08-17T14:00:00Z',
gameDate: '2026-08-17', gameIdFor: () => 'mlb:2026-08-17:StL@Cin',
});
const base = {
player: 'Andre Pallante', stat_type: 'strikeouts', line: 4.5, sport: 'mlb',
canonical_event_id: 'mlb:gamepk:824514',
event_identity_source: 'MLB_STATSAPI_GAMEPK',
event_identity_method: 'CANONICAL',
event_identity_version: 'evid@1',
event_occurrence: 1,
};
c.onGraded(base, [{ ...base, direction: 'over', grade: 'B', confidence: 60 }]);
expect(c.rows).toHaveLength(1);
expect(c.rows[0].canonical_event_id).toBe('mlb:gamepk:824514');
expect(c.rows[0].event_identity_source).toBe('MLB_STATSAPI_GAMEPK');
expect(c.rows[0].event_identity_method).toBe('CANONICAL');
expect(c.rows[0].event_occurrence).toBe(1);
});
test('a row with no identity carries NULL, never a fabricated id', () => {
const c = retention.createCollector({
snapshotId: 's1', capturedAt: 't', gameDate: '2026-08-17', gameIdFor: () => 'g',
});
const base = { player: 'X', stat_type: 'hits', line: 0.5, sport: 'wnba' };
c.onGraded(base, [{ ...base, direction: 'over', grade: 'B' }]);
expect(c.rows[0].canonical_event_id).toBeNull();
expect(c.rows[0].event_identity_method).toBeNull();
});
test('lineage keys on the canonical id once it arrives', () => {
const row = {
sport: 'mlb', game_date: '2026-08-17', player_key: 'andre pallante',
stat: 'strikeouts', side: 'over', line: 4.5, game_id: 'mlb:2026-08-17:StL@Cin',
captured_at: 't', published: true, grade: 'B', p_win: 0.6,
canonical_event_id: 'mlb:gamepk:824514',
};
expect(L.eventOccurrence(row)).toBe('mlb:gamepk:824514');
expect(L.readNaturalKey(row)).toContain('mlb:gamepk:824514');
const dropped = { ...row, canonical_event_id: null };
expect(L.readNaturalKey(dropped)).not.toContain('gamepk');
});
});
describe('IMPOSSIBLE-BINDING INVARIANT — the two verified regressions', () => {
const { nameKey } = require('../../src/utils/playerName');
const SLATE = [
{ gamePk: 823825, gameNumber: 1, gameDate: '2026-08-26T22:40:00Z', home: { team: 'Miami Marlins', teamId: 146 }, away: { team: 'Boston Red Sox', teamId: 111 } },
{ gamePk: 824878, gameNumber: 1, gameDate: '2026-08-26T23:15:00Z', home: { team: 'Atlanta Braves', teamId: 144 }, away: { team: 'Los Angeles Dodgers', teamId: 119 } },
{ gamePk: 824234, gameNumber: 1, gameDate: '2026-08-26T17:10:00Z', home: { team: 'Detroit Tigers', teamId: 116 }, away: { team: 'Tampa Bay Rays', teamId: 139 } },
{ gamePk: 824555, gameNumber: 1, gameDate: '2026-08-26T23:40:00Z', home: { team: 'Chicago White Sox', teamId: 145 }, away: { team: 'Texas Rangers', teamId: 140 } },
];
// Teams verified from statsapi: Joe Mack id 691788 Marlins, Yandy Diaz id 650490 Rays.
const TEAMS = new Map([
[nameKey('Joe Mack'), new Set(['MIA'])],
[nameKey('Yandy Diaz'), new Set(['TB'])],
]);
test('JOE MACK regression — Marlin bound to Dodgers@Braves is REFUSED', () => {
const prop = {
player: 'Joe Mack', stat_type: 'total_bases', line: 0.5, book: 'pinnacle',
home_team: 'Atlanta Braves', away_team: 'Los Angeles Dodgers',
game_date: '2026-08-26', game_time: '2026-08-26T23:15:00Z',
};
const r = E.resolveEvent('mlb', prop, SLATE, TEAMS);
expect(r.canonical_event_id).toBeNull();
expect(r.event_identity_method).toBe(E.IDENTITY_METHOD.UNRESOLVED);
expect(r.reason).toBe('impossible_binding');
expect(r.evidence).toMatchObject({ violated: true, player_teams: ['MIA'] });
expect(r.evidence.event_teams).toEqual(['LAD', 'ATL']);
});
test('YANDY DIAZ regression — Ray bound to Rangers@WhiteSox is REFUSED', () => {
const prop = {
player: 'Yandy Diaz', stat_type: 'total_bases', line: 0.5, book: 'pinnacle',
home_team: 'Chicago White Sox', away_team: 'Texas Rangers',
game_date: '2026-08-26', game_time: '2026-08-26T23:40:00Z',
};
const r = E.resolveEvent('mlb', prop, SLATE, TEAMS);
expect(r.reason).toBe('impossible_binding');
expect(r.evidence.player_teams).toEqual(['TB']);
});
test('each player STILL binds to his own real game', () => {
const mack = E.resolveEvent('mlb', {
player: 'Joe Mack', home_team: 'Miami Marlins', away_team: 'Boston Red Sox',
game_time: '2026-08-26T22:40:00Z',
}, SLATE, TEAMS);
expect(mack.canonical_event_id).toBe('mlb:gamepk:823825');
const diaz = E.resolveEvent('mlb', {
player: 'Yandy Diaz', home_team: 'Detroit Tigers', away_team: 'Tampa Bay Rays',
game_time: '2026-08-26T17:10:00Z',
}, SLATE, TEAMS);
expect(diaz.canonical_event_id).toBe('mlb:gamepk:824234');
});
test('UNKNOWN team preserves uncertainty — it is not a violation', () => {
// Refusing on absent evidence would drop real props for a data gap.
const r = E.resolveEvent('mlb', {
player: 'Someone Unlisted', home_team: 'Atlanta Braves',
away_team: 'Los Angeles Dodgers', game_time: '2026-08-26T23:15:00Z',
}, SLATE, TEAMS);
expect(r.canonical_event_id).toBe('mlb:gamepk:824878');
});
test('no index at all leaves behaviour exactly as before', () => {
const prop = {
player: 'Joe Mack', home_team: 'Atlanta Braves', away_team: 'Los Angeles Dodgers',
game_time: '2026-08-26T23:15:00Z',
};
expect(E.resolveEvent('mlb', prop, SLATE).canonical_event_id).toBe('mlb:gamepk:824878');
expect(E.resolveEvent('mlb', prop, SLATE, null).canonical_event_id).toBe('mlb:gamepk:824878');
});
test('time proximity CANNOT override an impossible team relationship', () => {
// The mis-nested event is the closest by start time; it must still refuse.
const prop = {
player: 'Joe Mack', home_team: 'Atlanta Braves', away_team: 'Los Angeles Dodgers',
game_time: '2026-08-26T23:15:00Z',
};
expect(E.resolveEvent('mlb', prop, SLATE, TEAMS).reason).toBe('impossible_binding');
});
test('a multi-team player (in-season trade) is not falsely refused', () => {
const traded = new Map([[nameKey('Joe Mack'), new Set(['MIA', 'ATL'])]]);
const r = E.resolveEvent('mlb', {
player: 'Joe Mack', home_team: 'Atlanta Braves', away_team: 'Los Angeles Dodgers',
game_time: '2026-08-26T23:15:00Z',
}, SLATE, traded);
expect(r.canonical_event_id).toBe('mlb:gamepk:824878');
});
test('the roster index keys on nameKey, not the raw name', () => {
// nameKey('Joe Mack') is 'joseph mack' — the nickname table. An index keyed
// on the raw string silently matches nothing and the invariant never fires.
expect(nameKey('Joe Mack')).toBe('joseph mack');
const rawKeyed = new Map([['joe mack', new Set(['MIA'])]]);
const r = E.resolveEvent('mlb', {
player: 'Joe Mack', home_team: 'Atlanta Braves', away_team: 'Los Angeles Dodgers',
game_time: '2026-08-26T23:15:00Z',
}, SLATE, rawKeyed);
expect(r.canonical_event_id).toBe('mlb:gamepk:824878'); // does NOT fire
});
test('buildPlayerTeamIndex keys correctly and survives an unreadable roster', async () => {
const built = await E.buildPlayerTeamIndex(SLATE, {
getTeamRoster: async (id) => {
if (id === 146) return [{ id: 691788, name: 'Joe Mack' }];
if (id === 144) throw new Error('roster unavailable');
return [];
},
});
expect(built.index.get(nameKey('Joe Mack'))).toEqual(new Set(['MIA']));
expect(built.stats.failed).toBe(1);
expect(built.stats.teams).toBeGreaterThan(0);
});
test('no roster source yields an empty index, never a fabricated one', async () => {
const built = await E.buildPlayerTeamIndex(SLATE, {});
expect(built.index.size).toBe(0);
});
});
describe('EVENT ADMISSION — a fractured identity is not model admission', () => {
const gs = require('../../src/services/gradeSlateService');
const admit = (props) => gs.__internals.admitForGrading(props, 'mlb');
const prop = (o) => ({ player: 'P', stat_type: 'hits', line: 0.5, book: 'draftkings', ...o });
test('RESOLVED with a canonical id is admitted', () => {
const r = admit([prop({ event_binding_status: 'RESOLVED', canonical_event_id: 'mlb:gamepk:1' })]);
expect(r.admitted).toHaveLength(1);
expect(r.rejected).toHaveLength(0);
});
test('RESOLVED WITHOUT a canonical id is still rejected', () => {
// The status alone is not the credential; the id has to be there.
const r = admit([prop({ event_binding_status: 'RESOLVED' })]);
expect(r.admitted).toHaveLength(0);
expect(r.rejected).toHaveLength(1);
});
test.each([
['UNRESOLVED', 'EVENT_UNRESOLVED'],
['AMBIGUOUS', 'EVENT_AMBIGUOUS'],
['CONTRADICTED', 'EVENT_PLAYER_TEAM_CONTRADICTION'],
])('%s does NOT grade and is labelled %s', (status, reason) => {
const r = admit([prop({ event_binding_status: status })]);
expect(r.admitted).toHaveLength(0);
expect(r.rejected[0].event_rejection_reason).toBe(reason);
expect(r.rejected[0].event_admission).toBe('REJECTED');
});
test('a legacy derived game_id can never satisfy the gate', () => {
const r = admit([prop({ event_binding_status: 'UNRESOLVED', game_id: 'mlb:2026-08-26:A@B' })]);
expect(r.admitted).toHaveLength(0);
});
test('a sport with no resolver is unchanged', () => {
expect(admit([prop({ event_binding_status: 'UNSUPPORTED' })]).admitted).toHaveLength(1);
expect(admit([prop({})]).admitted).toHaveLength(1);
});
test('rejected props are RETAINED for diagnostics, not discarded', () => {
const r = admit([prop({ event_binding_status: 'CONTRADICTED' })]);
expect(r.rejected).toHaveLength(1);
expect(r.reasons.EVENT_PLAYER_TEAM_CONTRADICTION).toBe(1);
});
test('the gate runs BEFORE dedupe and grading, exactly once', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/services/gradeSlateService.js'), 'utf8');
expect(src.indexOf('admitForGrading(props, sport)')).toBeLessThan(src.indexOf('dedupeProps(gate.admitted'));
// One admission seam, not scattered checks.
expect((src.match(/admitForGrading\(/g) || []).length).toBe(2); // definition + call
});
});
describe('ROSTER TEMPORAL SEMANTICS — evidence must describe the right moment', () => {
const { nameKey } = require('../../src/utils/playerName');
const SLATE = [
{ gamePk: 823825, gameNumber: 1, gameDate: '2026-08-26T22:40:00Z', home: { team: 'Miami Marlins', teamId: 146 }, away: { team: 'Boston Red Sox', teamId: 111 } },
{ gamePk: 824878, gameNumber: 1, gameDate: '2026-08-26T23:15:00Z', home: { team: 'Atlanta Braves', teamId: 144 }, away: { team: 'Los Angeles Dodgers', teamId: 119 } },
];
const TEAMS = new Map([[nameKey('Joe Mack'), new Set(['MIA'])]]);
const misNested = {
player: 'Joe Mack', stat_type: 'total_bases', line: 0.5, book: 'pinnacle',
home_team: 'Atlanta Braves', away_team: 'Los Angeles Dodgers',
game_date: '2026-08-26', game_time: '2026-08-26T23:15:00Z',
};
test('date-valid evidence yields CONTRADICTED', () => {
const r = E.resolveEvent('mlb', misNested, SLATE, TEAMS, true);
expect(r.event_binding_status).toBe(E.BINDING_STATUS.CONTRADICTED);
});
test('evidence that is NOT date-valid yields UNRESOLVED, never an accusation', () => {
const r = E.resolveEvent('mlb', misNested, SLATE, TEAMS, false);
expect(r.event_binding_status).toBe(E.BINDING_STATUS.UNRESOLVED);
expect(r.reason).toBe('participant_evidence_not_date_valid');
// Either way it does not grade — but the two verdicts stay distinct.
expect(r.canonical_event_id).toBeNull();
});
test('evidenceIsDateValid requires date-scoping AND a matching date', () => {
expect(E.evidenceIsDateValid({ date_scoped: true, as_of_date: '2026-08-26' }, '2026-08-26')).toBe(true);
expect(E.evidenceIsDateValid({ date_scoped: true, as_of_date: '2026-08-01' }, '2026-08-26')).toBe(false);
expect(E.evidenceIsDateValid({ date_scoped: false, as_of_date: null }, '2026-08-26')).toBe(false);
expect(E.evidenceIsDateValid(null, '2026-08-26')).toBe(false);
});
test('the roster fetch is scoped to the slate date', async () => {
const seen = [];
await E.buildPlayerTeamIndex(SLATE, {
asOfDate: '2026-08-26',
getTeamRoster: async (id, asOf) => { seen.push(asOf); return [{ id, name: 'Joe Mack' }]; },
});
expect(new Set(seen)).toEqual(new Set(['2026-08-26']));
});
test('an unscoped index reports date_scoped false', async () => {
const b = await E.buildPlayerTeamIndex(SLATE, {
getTeamRoster: async (id) => [{ id, name: 'Joe Mack' }],
});
expect(b.stats.date_scoped).toBe(false);
expect(E.evidenceIsDateValid(b.stats, '2026-08-26')).toBe(false);
});
test('the adapter passes the date through to statsapi', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/services/adapters/mlbStatsAdapter.js'), 'utf8');
expect(src).toMatch(/asOfDate \? `&date=/);
expect(src).toMatch(/mlbstats:roster:\$\{teamId\}:\$\{season\}:\$\{asOfDate \|\| 'now'\}/);
});
});
describe('MIS-NESTED MARKETS ARE REFUSED, NEVER RE-BOUND', () => {
const { nameKey } = require('../../src/utils/playerName');
const SLATE = [
{ gamePk: 823825, gameNumber: 1, gameDate: '2026-08-26T22:40:00Z', home: { team: 'Miami Marlins', teamId: 146 }, away: { team: 'Boston Red Sox', teamId: 111 } },
{ gamePk: 824878, gameNumber: 1, gameDate: '2026-08-26T23:15:00Z', home: { team: 'Atlanta Braves', teamId: 144 }, away: { team: 'Los Angeles Dodgers', teamId: 119 } },
{ gamePk: 824234, gameNumber: 1, gameDate: '2026-08-26T17:10:00Z', home: { team: 'Detroit Tigers', teamId: 116 }, away: { team: 'Tampa Bay Rays', teamId: 139 } },
{ gamePk: 824555, gameNumber: 1, gameDate: '2026-08-26T23:40:00Z', home: { team: 'Chicago White Sox', teamId: 145 }, away: { team: 'Texas Rangers', teamId: 140 } },
];
const TEAMS = new Map([
[nameKey('Joe Mack'), new Set(['MIA'])],
[nameKey('Yandy Diaz'), new Set(['TB'])],
]);
test('JOE MACK: contradicted, not re-bound to the Marlins game', () => {
const r = E.resolveEvent('mlb', {
player: 'Joe Mack', stat_type: 'total_bases', line: 0.5, book: 'pinnacle',
home_team: 'Atlanta Braves', away_team: 'Los Angeles Dodgers',
game_date: '2026-08-26', game_time: '2026-08-26T23:15:00Z',
}, SLATE, TEAMS, true);
expect(r.event_binding_status).toBe('CONTRADICTED');
// The refusal must NOT quietly relocate the prop to gamePk 823825. The
// provider record is invalid; inventing a different event is inventing
// provenance.
expect(r.canonical_event_id).toBeNull();
expect(r.canonical_event_id).not.toBe('mlb:gamepk:823825');
});
test('YANDY DIAZ: contradicted, not re-bound to the Rays game', () => {
const r = E.resolveEvent('mlb', {
player: 'Yandy Diaz', stat_type: 'total_bases', line: 0.5, book: 'pinnacle',
home_team: 'Chicago White Sox', away_team: 'Texas Rangers',
game_date: '2026-08-26', game_time: '2026-08-26T23:40:00Z',
}, SLATE, TEAMS, true);
expect(r.event_binding_status).toBe('CONTRADICTED');
expect(r.canonical_event_id).toBeNull();
expect(r.canonical_event_id).not.toBe('mlb:gamepk:824234');
});
test('each player CORRECTLY associated market remains eligible', () => {
const gs = require('../../src/services/gradeSlateService');
const good = [
{ player: 'Joe Mack', stat_type: 'total_bases', line: 0.5, book: 'draftkings', home_team: 'Miami Marlins', away_team: 'Boston Red Sox', game_time: '2026-08-26T22:40:00Z' },
{ player: 'Yandy Diaz', stat_type: 'total_bases', line: 0.5, book: 'draftkings', home_team: 'Detroit Tigers', away_team: 'Tampa Bay Rays', game_time: '2026-08-26T17:10:00Z' },
];
E.attachEventIdentity('mlb', good, SLATE, TEAMS, true);
for (const p of good) expect(p.event_binding_status).toBe('RESOLVED');
expect(gs.__internals.admitForGrading(good, 'mlb').admitted).toHaveLength(2);
});
test('the mis-nested props do not survive admission', () => {
const gs = require('../../src/services/gradeSlateService');
const bad = [
{ player: 'Joe Mack', stat_type: 'total_bases', line: 0.5, book: 'pinnacle', home_team: 'Atlanta Braves', away_team: 'Los Angeles Dodgers', game_time: '2026-08-26T23:15:00Z' },
{ player: 'Yandy Diaz', stat_type: 'total_bases', line: 0.5, book: 'pinnacle', home_team: 'Chicago White Sox', away_team: 'Texas Rangers', game_time: '2026-08-26T23:40:00Z' },
];
E.attachEventIdentity('mlb', bad, SLATE, TEAMS, true);
const gate = gs.__internals.admitForGrading(bad, 'mlb');
expect(gate.admitted).toHaveLength(0);
expect(gate.rejected).toHaveLength(2);
expect(gate.reasons.EVENT_PLAYER_TEAM_CONTRADICTION).toBe(2);
});
test('DOUBLEHEADER survives the stricter gate — both games still admitted', () => {
const gs = require('../../src/services/gradeSlateService');
const DH = [
{ gamePk: 824514, gameNumber: 1, gameDate: '2026-08-17T17:40:00Z', home: { team: 'Cincinnati Reds', teamId: 113 }, away: { team: 'St. Louis Cardinals', teamId: 138 } },
{ gamePk: 824478, gameNumber: 2, gameDate: '2026-08-17T22:40:00Z', home: { team: 'Cincinnati Reds', teamId: 113 }, away: { team: 'St. Louis Cardinals', teamId: 138 } },
];
const teams = new Map([[nameKey('Andre Pallante'), new Set(['STL'])]]);
const props = ['2026-08-17T17:40:00Z', '2026-08-17T22:40:00Z'].map((t) => ({
player: 'Andre Pallante', stat_type: 'strikeouts', line: 4.5, book: 'draftkings',
home_team: 'Cincinnati Reds', away_team: 'St. Louis Cardinals', game_time: t,
}));
E.attachEventIdentity('mlb', props, DH, teams, true);
expect(props.map((p) => p.event_binding_status)).toEqual(['RESOLVED', 'RESOLVED']);
expect(new Set(props.map((p) => p.canonical_event_id)).size).toBe(2);
expect(gs.__internals.admitForGrading(props, 'mlb').admitted).toHaveLength(2);
});
});