Path coverage: every branch from acquisition to the first grade callback

Audited the corridor rather than trusting the recorder. Nine upstream operations
sit between acquisition success and gradeAndCacheSlate — game binding, per-date
schedule fetch, roster index, attachEventIdentity, book-price capture, team-stat
refresh, hits-factor context, matchup keys — each with its own catch. NONE of
them early-returns, so the corridor always reaches the grader; but seven of them
were SILENT, and two could be misattributed.

COVERAGE WAS INCOMPLETE. Closed:
  * BINDING — bound / unresolved / already-had, and its throw
  * SCHEDULE — dates requested, game count, and its throw. A schedule outage
    previously surfaced as an EVENT IDENTITY error because it lands in that
    catch; it now records SCHEDULE_STAGE_ERROR and rethrows unchanged.
  * ROSTER — indexed players/teams/failed and evidence-date-validity, and its
    throw. Its catch only console.warn'd, so this stage was entirely invisible.
  * DEDUPE per-reason accounting off the filter's OWN branches: invalid fields /
    non-model book / duplicate identity / capped / not examined, plus
    model-book eligibility. Counts reconcile to the input exactly.
  * GRADE BOUNDARY — `reached` is derived from a candidate count and proves
    nothing. GRADE_LOOP_ENTERED, FIRST_GRADEBESTSIDE_STARTED and
    FIRST_ONGRADED_OBSERVED are now separate control-flow facts. No model
    output is recorded; a test greps for p_win/grade/confidence/edge/side.

Terminal states now name the stage: SCHEDULE_STAGE_ERROR, ROSTER_STAGE_ERROR,
BINDING_STAGE_ERROR, EVENT_IDENTITY_STAGE_ERROR, ADMISSION_STAGE_ERROR,
DEDUPE_STAGE_ERROR, IDENTITY_ALL_UNRESOLVED, ALL_REJECTED,
DEDUPE_ALL_NON_MODEL_BOOK, DEDUPE_ALL_INVALID_FIELDS, DEDUPE_EMPTY_OTHER,
READY_FOR_GRADING, GRADE_LOOP_STARTED, FIRST_GRADE_CALLBACK_OBSERVED.

TRACE COMPLETENESS INVARIANT. `reconcilePregrade` — acquisition NONZERO +
CONTINUED with no correlated downstream state is an OBSERVABILITY_GAP, never a
pipeline verdict. This programme has twice read an absence as a conclusion
("MLB exited at acquisition", "all props rejected at admission"); both were
wrong. Now it is a typed state with tests.

dedupeProps takes an OPTIONAL stats object and increments on the branches it
already takes, in the same order — reused, never reimplemented. Without the
object it is byte-identical; a test asserts that.

A PRODUCTION-BREAKING BUG CAUGHT BY THE FULL SUITE: the frozen no-op recorder
did not implement gradeStarted/firstOnGraded, so any caller without a recorder
threw inside the grade loop — and gradeAndCacheSlate's catch turned that into
{written:false,count:0}. Every slate would have graded NOTHING, silently. Fixed,
NO_PREGRADE now covers the full recorder surface, and a test asserts it does.

Exception semantics unchanged throughout: every added catch records and RETHROWS
the identical error. Admission rules, dedupe predicates, MODEL_BOOKS, event
identity, gameBinder, gradeBestSide and its arguments: 0 changed lines.
eventIdentity, oddsService, retentionService, gameBinder, bookRoles,
analyzeViaEngine1, probabilityEstimator, snapshotScheduler and ledgerService:
UNCHANGED. Zero new external calls — the only diff hit is the existing
getScheduleWithPitchers line re-indented into its own try.

Twelve teeth, injections verified present, against a green baseline of 89:
upstream catch silent (1) · missing trace as failure (1) · admission exception
as ALL_REJECTED (2) · non-model-book as duplicate (3) · dedupe-empty as
rejection (1) · falsely says grading started (6) · onGraded unrecorded (1) ·
sport overwrite (2) · intraday overwrite (1) · different attempt id (3) ·
observer adds an external call (1) · store failure changes outcome (2).
Restored byte-identically; teeth 3/4/6 re-run after the NO_PREGRADE fix.

The first teeth pass ran against a baseline the finer states had invalidated;
six superseded assertions were updated first and the run repeated.

388 suites / 5,269 tests pass. web tsc exit 0. Lineage stays OFF.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
This commit is contained in:
Kev
2026-08-28 01:21:09 -04:00
parent 8d741e3932
commit c1d9ec5bbb
6 changed files with 523 additions and 32 deletions
+44 -9
View File
@@ -58,7 +58,14 @@ const { isModelBook } = require('../config/bookRoles');
// statsapi is free and unlimited. Concurrency stays at 5 — one variable at a time. // statsapi is free and unlimited. Concurrency stays at 5 — one variable at a time.
// //
// Env-tunable so the ceiling can move without a deploy: GRADE_SLATE_LIMIT. // Env-tunable so the ceiling can move without a deploy: GRADE_SLATE_LIMIT.
const NO_PREGRADE = Object.freeze({ identity() {}, admission() {}, dedupe() {}, gradeLoop() {} }); // MUST implement EVERY recorder method. A missing one throws inside the grade
// loop, and gradeAndCacheSlate's catch turns that into {written:false,count:0}
// — i.e. a silently empty slate for every caller without a recorder.
const NO_PREGRADE = Object.freeze({
binding() {}, schedule() {}, roster() {},
identity() {}, admission() {}, dedupe() {}, gradeLoop() {},
gradeStarted() {}, firstOnGraded() {},
});
const DEFAULT_LIMIT = Number(process.env.GRADE_SLATE_LIMIT) > 0 const DEFAULT_LIMIT = Number(process.env.GRADE_SLATE_LIMIT) > 0
? Number(process.env.GRADE_SLATE_LIMIT) ? Number(process.env.GRADE_SLATE_LIMIT)
@@ -201,17 +208,31 @@ function propositionEventKey(p) {
return `legacy:${date}:${away}@${home}`; return `legacy:${date}:${away}@${home}`;
} }
function dedupeProps(props, limit) { function dedupeProps(props, limit, stats) {
const seen = new Set(); const seen = new Set();
const out = []; const out = [];
for (const p of props || []) { // DIAGNOSTIC COUNTERS ONLY. Incremented on the SAME branches the filter
if (!p || !p.player || !p.stat_type || p.line == null) continue; // already takes, in the same order, so the counts describe the real code path
if (!isModelBook(p.book)) continue; // rather than a second implementation of it. `stats` is optional; without it
// this function is byte-identical in behaviour.
const bump = (k) => { if (stats) stats[k] = (stats[k] || 0) + 1; };
const list = props || [];
let examined = 0;
for (const p of list) {
examined += 1;
if (!p || !p.player || !p.stat_type || p.line == null) { bump('dropped_invalid_fields'); continue; }
if (!isModelBook(p.book)) { bump('dropped_non_model_book'); continue; }
const key = `${propositionEventKey(p)}::${p.player}::${p.stat_type}::${p.line}`; const key = `${propositionEventKey(p)}::${p.player}::${p.stat_type}::${p.line}`;
if (seen.has(key)) continue; if (seen.has(key)) { bump('duplicate_identity_removed'); continue; }
seen.add(key); seen.add(key);
out.push(p); out.push(p);
if (out.length >= limit) break; if (out.length >= limit) { if (stats) stats.capped = true; break; }
}
if (stats) {
stats.input_count = list.length;
stats.output_count = out.length;
stats.model_book_eligible = out.length + (stats.duplicate_identity_removed || 0);
stats.not_examined = list.length - examined;
} }
return out; return out;
} }
@@ -382,8 +403,9 @@ async function gradeAndCacheSlate(sport, props, opts = {}) {
+ `${gate.rejected.length} rejected ${JSON.stringify(gate.reasons)}`); + `${gate.rejected.length} rejected ${JSON.stringify(gate.reasons)}`);
} }
let unique; let unique;
const dedupeStats = {};
try { try {
unique = dedupeProps(gate.admitted, limit); unique = dedupeProps(gate.admitted, limit, dedupeStats);
} catch (eDed) { } catch (eDed) {
pregrade.dedupe({ started: true, completed: false, threw: true, input_count: gate.admitted.length, error: eDed && eDed.message }); pregrade.dedupe({ started: true, completed: false, threw: true, input_count: gate.admitted.length, error: eDed && eDed.message });
throw eDed; throw eDed;
@@ -393,11 +415,24 @@ async function gradeAndCacheSlate(sport, props, opts = {}) {
input_count: gate.admitted.length, input_count: gate.admitted.length,
output_count: unique.length, output_count: unique.length,
removed_count: gate.admitted.length - unique.length, removed_count: gate.admitted.length - unique.length,
// Per-reason accounting straight off the filter's own branches.
dropped_invalid_fields: dedupeStats.dropped_invalid_fields || 0,
dropped_non_model_book: dedupeStats.dropped_non_model_book || 0,
duplicate_identity_removed: dedupeStats.duplicate_identity_removed || 0,
model_book_eligible: dedupeStats.model_book_eligible || 0,
non_model_book: dedupeStats.dropped_non_model_book || 0,
capped: !!dedupeStats.capped,
not_examined: dedupeStats.not_examined || 0,
}); });
pregrade.gradeLoop({ candidate_count: unique.length, reached: unique.length > 0 }); pregrade.gradeLoop({ candidate_count: unique.length, reached: unique.length > 0 });
if (unique.length === 0) return { written: false, count: 0 }; if (unique.length === 0) return { written: false, count: 0 };
const graded = (await mapLimit(unique, concurrency, (p) => gradeBestSide(grade, p, sport, opts))) const graded = (await mapLimit(unique, concurrency, (p) => {
// Proof that the loop actually began. `reached` above is derived from a
// count and cannot show this.
pregrade.gradeStarted();
return gradeBestSide(grade, p, sport, opts);
}))
.filter(Boolean) .filter(Boolean)
.sort((a, b) => (Number(b.confidence) || 0) - (Number(a.confidence) || 0)); .sort((a, b) => (Number(b.confidence) || 0) - (Number(a.confidence) || 0));
+80 -6
View File
@@ -237,15 +237,29 @@ async function history(sport, deps = {}) {
const PREGRADE_PREFIX = 'ops:pregrade:'; const PREGRADE_PREFIX = 'ops:pregrade:';
const PREGRADE_OUTCOME = Object.freeze({ const PREGRADE_OUTCOME = Object.freeze({
// Progress states — the boundary moves forward as each is proven.
READY_FOR_GRADING: 'READY_FOR_GRADING', READY_FOR_GRADING: 'READY_FOR_GRADING',
GRADE_LOOP_STARTED: 'GRADE_LOOP_STARTED',
FIRST_GRADE_CALLBACK_OBSERVED: 'FIRST_GRADE_CALLBACK_OBSERVED',
// Refusals — the cohort was deliberately emptied, and by which filter.
IDENTITY_ALL_UNRESOLVED: 'IDENTITY_ALL_UNRESOLVED',
ALL_REJECTED: 'ALL_REJECTED', ALL_REJECTED: 'ALL_REJECTED',
IDENTITY_STAGE_ERROR: 'IDENTITY_STAGE_ERROR', DEDUPE_ALL_NON_MODEL_BOOK: 'DEDUPE_ALL_NON_MODEL_BOOK',
DEDUPE_ALL_INVALID_FIELDS: 'DEDUPE_ALL_INVALID_FIELDS',
DEDUPE_EMPTY_OTHER: 'DEDUPE_EMPTY_OTHER',
// Exceptions — each names its own stage, never folded into a refusal.
SCHEDULE_STAGE_ERROR: 'SCHEDULE_STAGE_ERROR',
ROSTER_STAGE_ERROR: 'ROSTER_STAGE_ERROR',
BINDING_STAGE_ERROR: 'BINDING_STAGE_ERROR',
EVENT_IDENTITY_STAGE_ERROR: 'EVENT_IDENTITY_STAGE_ERROR',
ADMISSION_STAGE_ERROR: 'ADMISSION_STAGE_ERROR', ADMISSION_STAGE_ERROR: 'ADMISSION_STAGE_ERROR',
DEDUPE_STAGE_ERROR: 'DEDUPE_STAGE_ERROR', DEDUPE_STAGE_ERROR: 'DEDUPE_STAGE_ERROR',
DEDUPE_EMPTY: 'DEDUPE_EMPTY',
NO_INPUT_PROPS: 'NO_INPUT_PROPS', NO_INPUT_PROPS: 'NO_INPUT_PROPS',
OTHER_PREGRADING_ERROR: 'OTHER_PREGRADING_ERROR', OTHER_PREGRADING_ERROR: 'OTHER_PREGRADING_ERROR',
INCOMPLETE: 'INCOMPLETE', INCOMPLETE: 'INCOMPLETE',
// Reconciliation: acquisition CONTINUED but no downstream trace exists. This
// is an OBSERVABILITY failure, never evidence that the pipeline failed.
OBSERVABILITY_GAP: 'OBSERVABILITY_GAP',
}); });
function pregradeKey(sport) { function pregradeKey(sport) {
@@ -262,6 +276,9 @@ function beginPregrade({ attemptId, sport, trigger, codeSha, processStartedAt, p
process_generation: processStartedAt || null, process_generation: processStartedAt || null,
started_at: now || new Date().toISOString(), started_at: now || new Date().toISOString(),
input_props_count: Number.isFinite(propsCount) ? propsCount : null, input_props_count: Number.isFinite(propsCount) ? propsCount : null,
binding: null,
schedule: null,
roster: null,
identity: null, identity: null,
admission: null, admission: null,
dedupe: null, dedupe: null,
@@ -278,10 +295,21 @@ function beginPregrade({ attemptId, sport, trigger, codeSha, processStartedAt, p
function pregradeRecorder(trace) { function pregradeRecorder(trace) {
const t = trace || {}; const t = trace || {};
return { return {
binding(info) { t.binding = { ...info, error: sanitize(info && info.error) }; },
schedule(info) { t.schedule = { ...info, error: sanitize(info && info.error) }; },
roster(info) { t.roster = { ...info, error: sanitize(info && info.error) }; },
identity(info) { t.identity = { ...info, error: sanitize(info && info.error) }; }, identity(info) { t.identity = { ...info, error: sanitize(info && info.error) }; },
admission(info) { t.admission = { ...info, error: sanitize(info && info.error) }; }, admission(info) { t.admission = { ...info, error: sanitize(info && info.error) }; },
dedupe(info) { t.dedupe = { ...info, error: sanitize(info && info.error) }; }, dedupe(info) { t.dedupe = { ...info, error: sanitize(info && info.error) }; },
gradeLoop(info) { t.grade_loop = { ...info }; }, gradeLoop(info) { t.grade_loop = { ...t.grade_loop, ...info }; },
// CONTROL-FLOW FACTS ONLY. `reached` is derived from a candidate count and
// does NOT prove grading began; these two do. No model output is recorded.
gradeStarted() {
t.grade_loop = { ...t.grade_loop, entered: true, first_gradebestside_started: true };
},
firstOnGraded() {
t.grade_loop = { ...t.grade_loop, first_ongraded_observed: true };
},
}; };
} }
@@ -292,21 +320,66 @@ function pregradeRecorder(trace) {
*/ */
function classifyPregrade(trace) { function classifyPregrade(trace) {
const t = trace || {}; const t = trace || {};
if (t.identity && t.identity.threw) return PREGRADE_OUTCOME.IDENTITY_STAGE_ERROR; const gl = t.grade_loop || {};
// EXCEPTIONS FIRST, each naming its own stage. An exception is never folded
// into a refusal, and a refusal is never reported as an exception.
if (t.binding && t.binding.threw) return PREGRADE_OUTCOME.BINDING_STAGE_ERROR;
if (t.schedule && t.schedule.threw) return PREGRADE_OUTCOME.SCHEDULE_STAGE_ERROR;
if (t.roster && t.roster.threw) return PREGRADE_OUTCOME.ROSTER_STAGE_ERROR;
if (t.identity && t.identity.threw) return PREGRADE_OUTCOME.EVENT_IDENTITY_STAGE_ERROR;
if (t.admission && t.admission.threw) return PREGRADE_OUTCOME.ADMISSION_STAGE_ERROR; if (t.admission && t.admission.threw) return PREGRADE_OUTCOME.ADMISSION_STAGE_ERROR;
if (t.dedupe && t.dedupe.threw) return PREGRADE_OUTCOME.DEDUPE_STAGE_ERROR; if (t.dedupe && t.dedupe.threw) return PREGRADE_OUTCOME.DEDUPE_STAGE_ERROR;
if (t.input_props_count === 0) return PREGRADE_OUTCOME.NO_INPUT_PROPS; if (t.input_props_count === 0) return PREGRADE_OUTCOME.NO_INPUT_PROPS;
if (t.grade_loop && t.grade_loop.reached) return PREGRADE_OUTCOME.READY_FOR_GRADING; // PROGRESS, strongest proof first. `reached` alone is only a candidate count.
if (gl.first_ongraded_observed) return PREGRADE_OUTCOME.FIRST_GRADE_CALLBACK_OBSERVED;
if (gl.first_gradebestside_started) return PREGRADE_OUTCOME.GRADE_LOOP_STARTED;
if (gl.reached) return PREGRADE_OUTCOME.READY_FOR_GRADING;
// REFUSALS, naming the filter that emptied the cohort.
if (t.admission && t.admission.completed && t.admission.admitted_count === 0) { if (t.admission && t.admission.completed && t.admission.admitted_count === 0) {
// Name the CAUSE when identity explains it, rather than the symptom.
if (t.identity && t.identity.completed && t.identity.total > 0 && t.identity.canonical === 0) {
return PREGRADE_OUTCOME.IDENTITY_ALL_UNRESOLVED;
}
return PREGRADE_OUTCOME.ALL_REJECTED; return PREGRADE_OUTCOME.ALL_REJECTED;
} }
if (t.dedupe && t.dedupe.completed && t.dedupe.output_count === 0 if (t.dedupe && t.dedupe.completed && t.dedupe.output_count === 0
&& t.admission && t.admission.admitted_count > 0) { && t.admission && t.admission.admitted_count > 0) {
return PREGRADE_OUTCOME.DEDUPE_EMPTY; const d = t.dedupe;
if (d.dropped_non_model_book === d.input_count) return PREGRADE_OUTCOME.DEDUPE_ALL_NON_MODEL_BOOK;
if (d.dropped_invalid_fields === d.input_count) return PREGRADE_OUTCOME.DEDUPE_ALL_INVALID_FIELDS;
return PREGRADE_OUTCOME.DEDUPE_EMPTY_OTHER;
} }
return PREGRADE_OUTCOME.OTHER_PREGRADING_ERROR; return PREGRADE_OUTCOME.OTHER_PREGRADING_ERROR;
} }
/**
* TRACE COMPLETENESS INVARIANT.
*
* If acquisition recorded FINAL NONZERO + CONTINUED, a downstream state must
* exist. Its ABSENCE is an observability failure and must never be read as
* "the pipeline failed" — that inference is exactly what this programme got
* wrong twice.
*/
function reconcilePregrade(acqTrace, pgTrace) {
const a = acqTrace || null;
const continued = !!(a && a.final === FINAL.NONZERO && a.outcome === OUTCOME.CONTINUED);
if (!continued) return { applicable: false, complete: null, outcome: null };
if (!pgTrace) {
return { applicable: true, complete: false, outcome: PREGRADE_OUTCOME.OBSERVABILITY_GAP };
}
if (pgTrace.snapshot_attempt_id !== a.snapshot_attempt_id) {
return { applicable: true, complete: false, outcome: PREGRADE_OUTCOME.OBSERVABILITY_GAP, reason: 'attempt_id_mismatch' };
}
const undecided = !pgTrace.outcome
|| pgTrace.outcome === PREGRADE_OUTCOME.INCOMPLETE
|| pgTrace.outcome === PREGRADE_OUTCOME.OTHER_PREGRADING_ERROR;
return {
applicable: true,
complete: !undecided,
outcome: undecided ? PREGRADE_OUTCOME.OBSERVABILITY_GAP : pgTrace.outcome,
};
}
function finishPregrade(trace, { now } = {}) { function finishPregrade(trace, { now } = {}) {
const t = trace; const t = trace;
t.outcome = classifyPregrade(t); t.outcome = classifyPregrade(t);
@@ -328,6 +401,7 @@ async function pregradeHistory(sport, deps = {}) {
module.exports = { module.exports = {
TRIGGER, SOURCE_OUTCOME, FINAL, OUTCOME, PREGRADE_OUTCOME, TRIGGER, SOURCE_OUTCOME, FINAL, OUTCOME, PREGRADE_OUTCOME,
pregradeKey, beginPregrade, pregradeRecorder, classifyPregrade, finishPregrade, pregradeKey, beginPregrade, pregradeRecorder, classifyPregrade, finishPregrade,
reconcilePregrade,
persistPregrade, pregradeHistory, persistPregrade, pregradeHistory,
HISTORY_CAP, TTL_SECONDS, KEY_PREFIX, HISTORY_CAP, TTL_SECONDS, KEY_PREFIX,
key, sanitize, newAttemptId, key, sanitize, newAttemptId,
+32 -1
View File
@@ -516,6 +516,10 @@ async function runSnapshot(sport, opts = {}) {
try { try {
const binder = deps.gameBinder || require('./gameBinder'); const binder = deps.gameBinder || require('./gameBinder');
const b = await binder.attachGameTimes(sp, props, { gradedAt: ts }); const b = await binder.attachGameTimes(sp, props, { gradedAt: ts });
pgRec.binding({
started: true, completed: true, threw: false,
bound: b.bound, unresolved: b.unresolved, already_had: b.alreadyHad,
});
console.log(`[snapshot] game binding ${sp}: ${b.bound} bound, ${b.alreadyHad} already had times, ${b.unresolved} UNRESOLVED, ${b.ambiguous} ambiguous(doubleheader)`); console.log(`[snapshot] game binding ${sp}: ${b.bound} bound, ${b.alreadyHad} already had times, ${b.unresolved} UNRESOLVED, ${b.ambiguous} ambiguous(doubleheader)`);
// CANONICAL EVENT IDENTITY (MLB). The bound game_time above is what makes // CANONICAL EVENT IDENTITY (MLB). The bound game_time above is what makes
@@ -532,11 +536,20 @@ async function runSnapshot(sport, opts = {}) {
const mlbAdapter = deps.mlbAdapter || require('./adapters/mlbStatsAdapter'); const mlbAdapter = deps.mlbAdapter || require('./adapters/mlbStatsAdapter');
const dates = [...new Set(props.map((p) => p && p.game_date).filter(Boolean))]; const dates = [...new Set(props.map((p) => p && p.game_date).filter(Boolean))];
const games = []; const games = [];
pgRec.schedule({ started: true, completed: false, threw: false, dates_requested: dates.length });
try {
for (const d of dates) { for (const d of dates) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const g = await mlbAdapter.getScheduleWithPitchers(d); const g = await mlbAdapter.getScheduleWithPitchers(d);
if (Array.isArray(g)) games.push(...g); if (Array.isArray(g)) games.push(...g);
} }
} catch (eSch) {
// Record and RETHROW: the existing identity catch below still handles
// it exactly as before. Without this a schedule outage was reported as
// an event-identity error.
pgRec.schedule({ started: true, completed: false, threw: true, dates_requested: dates.length, error: eSch && eSch.message });
throw eSch;
}
// PARTICIPANT EVIDENCE. The prop's own team fields cannot police this: // PARTICIPANT EVIDENCE. The prop's own team fields cannot police this:
// when the provider nests a player under the wrong event, those fields // when the provider nests a player under the wrong event, those fields
// ARE the wrong event's teams. The player's real team has to come from // ARE the wrong event's teams. The player's real team has to come from
@@ -545,6 +558,10 @@ async function runSnapshot(sport, opts = {}) {
// The roster is fetched AS OF THE SLATE'S DATE, not "now". statsapi // The roster is fetched AS OF THE SLATE'S DATE, not "now". statsapi
// honours a `date` parameter and it genuinely changes the answer, so // honours a `date` parameter and it genuinely changes the answer, so
// the evidence describes the moment being judged. // the evidence describes the moment being judged.
pgRec.schedule({
started: true, completed: true, threw: false,
dates_requested: dates.length, games: games.length,
});
const slateDate = dates.length === 1 ? dates[0] : null; const slateDate = dates.length === 1 ? dates[0] : null;
let playerTeams = null; let playerTeams = null;
let evidenceDateValid = false; let evidenceDateValid = false;
@@ -555,11 +572,17 @@ async function runSnapshot(sport, opts = {}) {
}); });
playerTeams = built.index; playerTeams = built.index;
evidenceDateValid = evid.evidenceIsDateValid(built.stats, slateDate); evidenceDateValid = evid.evidenceIsDateValid(built.stats, slateDate);
pgRec.roster({
started: true, completed: true, threw: false,
players: built.stats.players, teams: built.stats.teams,
failed: built.stats.failed || 0, evidence_date_valid: evidenceDateValid,
});
console.log(`[snapshot] participant evidence ${sp}: ${built.stats.players} players` console.log(`[snapshot] participant evidence ${sp}: ${built.stats.players} players`
+ ` across ${built.stats.teams} rosters as of ${slateDate || 'unscoped'}` + ` across ${built.stats.teams} rosters as of ${slateDate || 'unscoped'}`
+ `${built.stats.failed ? `, ${built.stats.failed} unreadable` : ''}` + `${built.stats.failed ? `, ${built.stats.failed} unreadable` : ''}`
+ ` · date-valid=${evidenceDateValid}`); + ` · date-valid=${evidenceDateValid}`);
} catch (e3) { } catch (e3) {
pgRec.roster({ started: true, completed: false, threw: true, error: e3 && e3.message });
console.warn(`[snapshot] participant evidence unavailable for ${sp}:`, e3.message); console.warn(`[snapshot] participant evidence unavailable for ${sp}:`, e3.message);
} }
// Without date-valid evidence a conflict can only be UNRESOLVED, never // Without date-valid evidence a conflict can only be UNRESOLVED, never
@@ -589,6 +612,7 @@ async function runSnapshot(sport, opts = {}) {
}); });
} }
} catch (e) { } catch (e) {
pgRec.binding({ started: true, completed: false, threw: true, error: e && e.message });
console.warn(`[snapshot] game binding failed for ${sp} (rows without a real game time will be skipped):`, e.message); console.warn(`[snapshot] game binding failed for ${sp} (rows without a real game time will be skipped):`, e.message);
} }
@@ -698,6 +722,9 @@ async function runSnapshot(sport, opts = {}) {
} }
} }
// Records whether the hook existed at all, so `first_ongraded_observed:false`
// can be told apart from "no collector was attached".
pgRec.gradeLoop({ ongraded_hook_attached: !!collector });
await deps.gradeAndCacheSlate(sp, props, { await deps.gradeAndCacheSlate(sp, props, {
pregrade: pgRec, pregrade: pgRec,
factorContext, factorContext,
@@ -710,8 +737,12 @@ async function runSnapshot(sport, opts = {}) {
source: (odds && odds.provider) || 'odds-api', source: (odds && odds.provider) || 'odds-api',
now: deps.now, now: deps.now,
cacheSet: async (_k, v) => { envelope = v; }, cacheSet: async (_k, v) => { envelope = v; },
onGraded: collector ? collector.onGraded : undefined, // Wrapped ONLY when a collector exists, so the downstream
// `typeof opts.onGraded === 'function'` branch is exactly as before. The
// wrapper records a control-flow fact and delegates unchanged.
onGraded: collector ? ((base, sides) => { pgRec.firstOnGraded(); return collector.onGraded(base, sides); }) : undefined,
onPublished: collector ? collector.onPublished : undefined, onPublished: collector ? collector.onPublished : undefined,
}); });
// Persist the pre-grading stage trace. Best-effort at the call site: a // Persist the pre-grading stage trace. Best-effort at the call site: a
// telemetry failure must never fail a healthy slate. // telemetry failure must never fail a healthy slate.
+337
View File
@@ -0,0 +1,337 @@
'use strict';
/**
* PATH COVERAGE — acquisition success to the first real onGraded callback.
*
* Every branch in that corridor must explain itself. Twice now this programme
* has read an absence as a conclusion: first "MLB exited at acquisition" (from
* a missing retention record), then "all props rejected at admission" (from an
* empty collector). Both were wrong. So the rule here is that a MISSING trace
* is an OBSERVABILITY_GAP, never a pipeline verdict.
*/
const fs = require('fs');
const path = require('path');
const acq = require('../../src/services/ops/acquisitionTrace');
const snap = require('../../src/services/snapshotService');
const { __internals } = require('../../src/services/gradeSlateService');
const { MODEL_BOOKS } = require('../../src/config/bookRoles');
const ROOT = path.resolve(__dirname, '..', '..');
const BOOK = [...MODEL_BOOKS][0];
jest.setTimeout(20000);
const P = (n, over = {}) => Array.from({ length: n }, (_, i) => ({
player: `P${i}`, stat_type: 'hits', line: 0.5 + i, sport: 'mlb', book: BOOK,
home_team: 'Reds', away_team: 'Cardinals', game_date: '2026-08-28', ...over,
}));
const identityOk = (counts = {}) => ({
buildPlayerTeamIndex: async () => ({ index: {}, stats: { players: 12, teams: 2, failed: 0 } }),
evidenceIsDateValid: () => true,
attachEventIdentity: () => ({ total: 4, canonical: 4, unresolved: 0, unsupported: 0, impossible: 0, reasons: {}, ...counts }),
});
/** Runs the REAL runSnapshot corridor; grading is stubbed unless asked for. */
async function corridor(over = {}) {
const pg = []; const a = [];
await snap.runSnapshot('mlb', {
getOdds: async () => ({ sport: 'mlb', props: P(4), provider: 'propline', source: 'cache' }),
gradeAndCacheSlate: async () => ({ written: false, count: 0 }),
gameBinder: { attachGameTimes: async () => ({ bound: 4, unresolved: 0, alreadyHad: 0 }) },
mlbAdapter: { getScheduleWithPitchers: async () => [{ gamePk: 1 }, { gamePk: 2 }] },
eventIdentity: identityOk(),
notify: async () => {}, sleep: async () => {}, retryDelayMs: 0,
now: () => '2026-08-28T14:00:00.000Z', scheduledHourUtc: 14, processStartedAt: 'proc-1',
retention: null,
ledger: { recordPipelineGrades: async () => {}, captureClosing: async () => {}, gameDateFor: () => '2026-08-28' },
captureBookPrices: async () => ({}), buildEspnIndex: async () => ({}),
refreshTeamStats: async () => ({ captured: 0 }),
cacheGet: async () => null, cacheSet: async () => {},
persistAcquisitionTrace: async (t) => a.push(t),
persistPregradeTrace: async (t) => pg.push(t),
...over,
});
return { pg: pg[0], acq: a[0] };
}
describe('UPSTREAM MLB STAGES', () => {
test('SCHEDULE FETCH ERROR names the schedule stage, not identity', async () => {
const { pg } = await corridor({ mlbAdapter: { getScheduleWithPitchers: async () => { throw new Error('statsapi 503'); } } });
expect(pg.schedule.threw).toBe(true);
expect(pg.schedule.error).toMatch(/statsapi 503/);
expect(pg.outcome).toBe(acq.PREGRADE_OUTCOME.SCHEDULE_STAGE_ERROR);
expect(pg.outcome).not.toBe(acq.PREGRADE_OUTCOME.EVENT_IDENTITY_STAGE_ERROR);
});
test('SCHEDULE SUCCESS records dates requested and game count', async () => {
const { pg } = await corridor();
expect(pg.schedule.completed).toBe(true);
expect(pg.schedule.dates_requested).toBe(1);
expect(pg.schedule.games).toBe(2);
});
test('ROSTER INDEX ERROR is visible — its catch used to swallow silently', async () => {
const { pg } = await corridor({
eventIdentity: { ...identityOk(), buildPlayerTeamIndex: async () => { throw new Error('roster 500'); } },
});
expect(pg.roster.threw).toBe(true);
expect(pg.roster.error).toMatch(/roster 500/);
expect(pg.outcome).toBe(acq.PREGRADE_OUTCOME.ROSTER_STAGE_ERROR);
});
test('ROSTER SUCCESS records the indexed counts', async () => {
const { pg } = await corridor();
expect(pg.roster.completed).toBe(true);
expect(pg.roster.players).toBe(12);
expect(pg.roster.teams).toBe(2);
expect(pg.roster.evidence_date_valid).toBe(true);
});
test('BINDING ERROR names the binding stage', async () => {
const { pg } = await corridor({ gameBinder: { attachGameTimes: async () => { throw new Error('binder boom'); } } });
expect(pg.binding.threw).toBe(true);
expect(pg.outcome).toBe(acq.PREGRADE_OUTCOME.BINDING_STAGE_ERROR);
});
test('BINDING SUCCESS records bound / unresolved / already-had', async () => {
const { pg } = await corridor({ gameBinder: { attachGameTimes: async () => ({ bound: 3, unresolved: 1, alreadyHad: 0 }) } });
expect(pg.binding).toMatchObject({ completed: true, bound: 3, unresolved: 1, already_had: 0 });
});
test('EVENT IDENTITY ERROR is recorded and the existing fallback is preserved', async () => {
const { pg } = await corridor({
eventIdentity: { ...identityOk(), attachEventIdentity: () => { throw new Error('identity boom'); } },
});
expect(pg.identity.threw).toBe(true);
expect(pg.outcome).toBe(acq.PREGRADE_OUTCOME.EVENT_IDENTITY_STAGE_ERROR);
// The existing catch keeps the slate alive — props stay on legacy identity.
const src = fs.readFileSync(path.join(ROOT, 'src/services/snapshotService.js'), 'utf8');
expect(src).toMatch(/Identity is additive: a failure leaves props on legacy identity/);
});
test('IDENTITY ALL UNRESOLVED reports truthful status counts', async () => {
const { pg } = await corridor({
eventIdentity: identityOk({ total: 4, canonical: 0, unresolved: 4, impossible: 0, reasons: { no_team_match: 4 } }),
gradeAndCacheSlate: async (sp, props, o) => {
o.pregrade.admission({ started: true, completed: true, threw: false, input_count: 4, admitted_count: 0, rejected_count: 4, rejection_reason_counts: { EVENT_UNRESOLVED: 4 } });
return { written: false, count: 0 };
},
});
expect(pg.identity).toMatchObject({ completed: true, total: 4, canonical: 0, unresolved: 4 });
expect(pg.identity.reasons.no_team_match).toBe(4);
// Names the CAUSE, not just the symptom.
expect(pg.outcome).toBe(acq.PREGRADE_OUTCOME.IDENTITY_ALL_UNRESOLVED);
});
});
describe('DEDUPE IS A FILTER — exact drop accounting off its own branches', () => {
const d = (rows, limit = 100) => { const st = {}; const out = __internals.dedupeProps(rows, limit, st); return { out, st }; };
test('behaviour is byte-identical with and without the stats object', () => {
const rows = [...P(3), ...P(2, { book: 'nope' })];
expect(__internals.dedupeProps(rows, 100).length).toBe(d(rows).out.length);
});
test('ALL dropped by MODEL BOOK', () => {
const { out, st } = d(P(5, { book: 'not_a_model_book' }));
expect(out).toHaveLength(0);
expect(st.dropped_non_model_book).toBe(5);
expect(st.dropped_invalid_fields || 0).toBe(0);
});
test('ALL dropped by MISSING REQUIRED FIELDS', () => {
const { out, st } = d(P(4).map((p) => ({ ...p, player: null })));
expect(out).toHaveLength(0);
expect(st.dropped_invalid_fields).toBe(4);
expect(st.dropped_non_model_book || 0).toBe(0);
});
test('only TRUE duplicates counted as duplicates', () => {
const one = P(1);
const { out, st } = d([...one, ...one, ...one]);
expect(out).toHaveLength(1);
expect(st.duplicate_identity_removed).toBe(2);
expect(st.dropped_non_model_book || 0).toBe(0);
});
test('counts reconcile to the input exactly', () => {
const rows = [...P(2), ...P(2), ...P(1, { book: 'x' }), { player: null }];
const { out, st } = d(rows);
const accounted = out.length
+ (st.dropped_invalid_fields || 0) + (st.dropped_non_model_book || 0)
+ (st.duplicate_identity_removed || 0) + (st.not_examined || 0);
expect(accounted).toBe(rows.length);
expect(st.input_count).toBe(rows.length);
});
test('MODEL-book eligibility is countable for the cohort', () => {
const { st } = d([...P(3), ...P(2, { book: 'nope' })]);
expect(st.model_book_eligible).toBe(3);
expect(st.dropped_non_model_book).toBe(2);
});
test('the cap records not_examined rather than pretending it saw everything', () => {
const { out, st } = d(P(10), 4);
expect(out).toHaveLength(4);
expect(st.capped).toBe(true);
expect(st.not_examined).toBe(6);
});
});
describe('THE GRADING BOUNDARY', () => {
const realGrade = () => require('../../src/services/gradeSlateService').gradeAndCacheSlate;
test('candidates > 0 but loop NOT entered is distinct from loop entered', () => {
const t = acq.beginPregrade({ attemptId: 'a', sport: 'mlb', propsCount: 4 });
acq.pregradeRecorder(t).gradeLoop({ candidate_count: 4, reached: true });
expect(acq.finishPregrade(t, {}).outcome).toBe(acq.PREGRADE_OUTCOME.READY_FOR_GRADING);
expect(t.grade_loop.first_gradebestside_started).toBeUndefined();
});
test('FIRST gradeBestSide START is proven through the REAL loop', async () => {
const t = acq.beginPregrade({ attemptId: 'a', sport: 'mlb', propsCount: 3 });
await realGrade()('mlb', P(3), {
pregrade: acq.pregradeRecorder(t), cacheSet: async () => {}, grade: async () => null,
});
expect(t.grade_loop.entered).toBe(true);
expect(t.grade_loop.first_gradebestside_started).toBe(true);
expect(acq.finishPregrade(t, {}).outcome).toBe(acq.PREGRADE_OUTCOME.GRADE_LOOP_STARTED);
});
test('FIRST onGraded is proven separately from gradeBestSide starting', async () => {
const t = acq.beginPregrade({ attemptId: 'a', sport: 'mlb', propsCount: 3 });
const rec = acq.pregradeRecorder(t);
await realGrade()('mlb', P(3), {
pregrade: rec, cacheSet: async () => {}, grade: async () => null,
onGraded: () => { rec.firstOnGraded(); },
});
expect(t.grade_loop.first_gradebestside_started).toBe(true);
expect(t.grade_loop.first_ongraded_observed).toBe(true);
expect(acq.finishPregrade(t, {}).outcome).toBe(acq.PREGRADE_OUTCOME.FIRST_GRADE_CALLBACK_OBSERVED);
});
test('gradeBestSide starting WITHOUT onGraded is its own state', async () => {
const t = acq.beginPregrade({ attemptId: 'a', sport: 'mlb', propsCount: 3 });
await realGrade()('mlb', P(3), {
pregrade: acq.pregradeRecorder(t), cacheSet: async () => {}, grade: async () => null,
});
expect(t.grade_loop.first_gradebestside_started).toBe(true);
expect(t.grade_loop.first_ongraded_observed).toBeUndefined();
expect(acq.finishPregrade(t, {}).outcome).not.toBe(acq.PREGRADE_OUTCOME.FIRST_GRADE_CALLBACK_OBSERVED);
});
test('runSnapshot wraps onGraded ONLY when a collector exists', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/services/snapshotService.js'), 'utf8');
expect(src).toMatch(/onGraded: collector \? \(\(base, sides\) => \{ pgRec\.firstOnGraded\(\); return collector\.onGraded\(base, sides\); \}\) : undefined/);
expect(src).toMatch(/ongraded_hook_attached: !!collector/);
});
test('no model output is recorded — control-flow facts only', () => {
const t = acq.beginPregrade({ attemptId: 'a', sport: 'mlb' });
const rec = acq.pregradeRecorder(t);
rec.gradeStarted(); rec.firstOnGraded();
const blob = JSON.stringify(t);
for (const forbidden of ['p_win', 'grade', 'confidence', 'projection', 'edge', 'side']) {
expect(blob).not.toMatch(new RegExp(`"${forbidden}"`));
}
});
});
describe('TRACE COMPLETENESS INVARIANT', () => {
const acqOk = { snapshot_attempt_id: 'acq_1', final: acq.FINAL.NONZERO, outcome: acq.OUTCOME.CONTINUED };
test('acquisition CONTINUED + NO downstream trace = OBSERVABILITY_GAP, not failure', () => {
const r = acq.reconcilePregrade(acqOk, null);
expect(r.applicable).toBe(true);
expect(r.complete).toBe(false);
expect(r.outcome).toBe(acq.PREGRADE_OUTCOME.OBSERVABILITY_GAP);
});
test('a mismatched attempt id is also a gap, never a verdict', () => {
const r = acq.reconcilePregrade(acqOk, { snapshot_attempt_id: 'other', outcome: acq.PREGRADE_OUTCOME.ALL_REJECTED });
expect(r.outcome).toBe(acq.PREGRADE_OUTCOME.OBSERVABILITY_GAP);
expect(r.reason).toBe('attempt_id_mismatch');
});
test('an undecided downstream state is a gap, not a conclusion', () => {
for (const o of [undefined, acq.PREGRADE_OUTCOME.INCOMPLETE, acq.PREGRADE_OUTCOME.OTHER_PREGRADING_ERROR]) {
const r = acq.reconcilePregrade(acqOk, { snapshot_attempt_id: 'acq_1', outcome: o });
expect(r.outcome).toBe(acq.PREGRADE_OUTCOME.OBSERVABILITY_GAP);
}
});
test('a decided downstream state reconciles complete', () => {
const r = acq.reconcilePregrade(acqOk, { snapshot_attempt_id: 'acq_1', outcome: acq.PREGRADE_OUTCOME.ALL_REJECTED });
expect(r.complete).toBe(true);
expect(r.outcome).toBe(acq.PREGRADE_OUTCOME.ALL_REJECTED);
});
test('a stubbed/short-circuited grader that records nothing IS a gap', async () => {
// Proves the invariant fires rather than inventing a verdict from silence.
const { pg, acq: a } = await corridor();
expect(acq.reconcilePregrade(a, pg).outcome).toBe(acq.PREGRADE_OUTCOME.OBSERVABILITY_GAP);
});
test('it does not apply when acquisition did not continue', () => {
const r = acq.reconcilePregrade({ snapshot_attempt_id: 'a', final: acq.FINAL.THREW, outcome: acq.OUTCOME.EARLY_RETURN_ODDS_ERROR }, null);
expect(r.applicable).toBe(false);
});
test('a real corridor run produces a decided state and the SAME attempt id', async () => {
// The REAL grader, so admission/dedupe/grade-loop actually record. With a
// stubbed grader nothing records and the reconciliation correctly reports a
// gap — which is the invariant working, not a decided state.
const { pg, acq: a } = await corridor({
gradeAndCacheSlate: require('../../src/services/gradeSlateService').gradeAndCacheSlate,
grade: async () => null,
});
expect(pg.snapshot_attempt_id).toBe(a.snapshot_attempt_id);
const r = acq.reconcilePregrade(a, pg);
expect(r.complete).toBe(true);
expect(r.outcome).not.toBe(acq.PREGRADE_OUTCOME.OBSERVABILITY_GAP);
});
});
describe('THE NO-OP RECORDER MUST IMPLEMENT EVERY METHOD', () => {
// A missing method throws inside the grade loop, and the enclosing catch
// turns that into {written:false,count:0} — a silently empty slate for every
// caller that passes no recorder. This shipped once; it must not again.
test('NO_PREGRADE covers the full recorder surface', async () => {
const real = acq.pregradeRecorder(acq.beginPregrade({ attemptId: 'a', sport: 'mlb' }));
const src = fs.readFileSync(path.join(ROOT, 'src/services/gradeSlateService.js'), 'utf8');
const i = src.indexOf('const NO_PREGRADE');
const block = src.slice(i, src.indexOf('\n});', i) + 4);
for (const m of Object.keys(real)) {
expect(block).toContain(`${m}(`);
}
});
test('grading works with NO recorder passed at all', async () => {
const res = await require('../../src/services/gradeSlateService')
.gradeAndCacheSlate('mlb', P(2), { cacheSet: async () => {}, grade: async () => null });
// Refusals produce no grades, but it must NOT fail with a recorder error.
expect(res.error).toBeUndefined();
});
});
describe('ZERO NEW EXTERNAL CALLS', () => {
test('the observer adds no provider or schedule call', () => {
const snapSrc = fs.readFileSync(path.join(ROOT, 'src/services/snapshotService.js'), 'utf8');
// getScheduleWithPitchers is called exactly where the product already called it.
expect((snapSrc.match(/getScheduleWithPitchers\(/g) || []).length).toBe(2);
expect((snapSrc.match(/buildPlayerTeamIndex\(/g) || []).length).toBe(1);
const traceSrc = fs.readFileSync(path.join(ROOT, 'src/services/ops/acquisitionTrace.js'), 'utf8');
const code = traceSrc.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1')
.replace(/s\.replace\([\s\S]*?\);/g, '');
for (const f of ['fetch(', 'axios', 'getSchedule', 'getProps', 'getOdds', 'oddsService']) {
expect(code).not.toContain(f);
}
});
test('a trace-store failure never changes the product outcome', async () => {
const { pg } = await corridor({ persistPregradeTrace: async () => { throw new Error('redis down'); } });
expect(pg).toBeUndefined(); // nothing stored…
// …and the run still completed: no throw escaped runSnapshot.
});
});
+23 -11
View File
@@ -48,8 +48,11 @@ describe('THE STAGE MATRIX', () => {
expect(trace.admission.rejected_count).toBe(0); expect(trace.admission.rejected_count).toBe(0);
expect(trace.dedupe.completed).toBe(true); expect(trace.dedupe.completed).toBe(true);
expect(trace.dedupe.output_count).toBe(5); expect(trace.dedupe.output_count).toBe(5);
expect(trace.grade_loop).toEqual({ candidate_count: 5, reached: true }); expect(trace.grade_loop).toMatchObject({ candidate_count: 5, reached: true });
expect(trace.outcome).toBe(acq.PREGRADE_OUTCOME.READY_FOR_GRADING); // The loop now proves it ENTERED, which is strictly stronger than a
// candidate count, so the terminal state advances past READY_FOR_GRADING.
expect(trace.grade_loop.first_gradebestside_started).toBe(true);
expect(trace.outcome).toBe(acq.PREGRADE_OUTCOME.GRADE_LOOP_STARTED);
}); });
test('ALL_REJECTED — with EXACT reason accounting', async () => { test('ALL_REJECTED — with EXACT reason accounting', async () => {
@@ -74,7 +77,7 @@ describe('THE STAGE MATRIX', () => {
expect(trace.admission.admitted_count).toBe(4); expect(trace.admission.admitted_count).toBe(4);
expect(trace.admission.rejected_count).toBe(3); expect(trace.admission.rejected_count).toBe(3);
expect(trace.admission.rejection_reason_counts.EVENT_PLAYER_TEAM_CONTRADICTION).toBe(3); expect(trace.admission.rejection_reason_counts.EVENT_PLAYER_TEAM_CONTRADICTION).toBe(3);
expect(trace.outcome).toBe(acq.PREGRADE_OUTCOME.READY_FOR_GRADING); expect(trace.outcome).toBe(acq.PREGRADE_OUTCOME.GRADE_LOOP_STARTED);
}); });
test('DEDUPE_EMPTY — admitted > 0 but dedupe filters to zero, NOT ALL_REJECTED', async () => { test('DEDUPE_EMPTY — admitted > 0 but dedupe filters to zero, NOT ALL_REJECTED', async () => {
@@ -84,7 +87,9 @@ describe('THE STAGE MATRIX', () => {
expect(trace.admission.admitted_count).toBe(5); expect(trace.admission.admitted_count).toBe(5);
expect(trace.dedupe.input_count).toBe(5); expect(trace.dedupe.input_count).toBe(5);
expect(trace.dedupe.output_count).toBe(0); expect(trace.dedupe.output_count).toBe(0);
expect(trace.outcome).toBe(acq.PREGRADE_OUTCOME.DEDUPE_EMPTY); // Now names WHICH filter emptied it, not merely that it emptied.
expect(trace.dedupe.dropped_non_model_book).toBe(5);
expect(trace.outcome).toBe(acq.PREGRADE_OUTCOME.DEDUPE_ALL_NON_MODEL_BOOK);
expect(trace.outcome).not.toBe(acq.PREGRADE_OUTCOME.ALL_REJECTED); expect(trace.outcome).not.toBe(acq.PREGRADE_OUTCOME.ALL_REJECTED);
}); });
@@ -126,7 +131,9 @@ describe('THE STAGE MATRIX', () => {
await snap2.runSnapshot('mlb', { await snap2.runSnapshot('mlb', {
getOdds: async () => ({ sport: 'mlb', props: props(2), provider: 'propline', source: 'cache' }), getOdds: async () => ({ sport: 'mlb', props: props(2), provider: 'propline', source: 'cache' }),
gradeAndCacheSlate: async () => ({ written: false, count: 0 }), gradeAndCacheSlate: async () => ({ written: false, count: 0 }),
eventIdentity: { buildPlayerTeamIndex: async () => { throw new Error('roster down'); }, // Roster succeeds so the IDENTITY throw is the one under test; roster now
// has its own terminal state and would otherwise (correctly) outrank it.
eventIdentity: { buildPlayerTeamIndex: async () => ({ index: {}, stats: { players: 1, teams: 1, failed: 0 } }),
evidenceIsDateValid: () => false, evidenceIsDateValid: () => false,
attachEventIdentity: () => { throw new Error('statsapi shape changed'); } }, attachEventIdentity: () => { throw new Error('statsapi shape changed'); } },
mlbAdapter: { getScheduleWithPitchers: async () => [] }, mlbAdapter: { getScheduleWithPitchers: async () => [] },
@@ -141,14 +148,14 @@ describe('THE STAGE MATRIX', () => {
expect(pg).toHaveLength(1); expect(pg).toHaveLength(1);
expect(pg[0].identity).toBeTruthy(); expect(pg[0].identity).toBeTruthy();
expect(pg[0].identity.threw).toBe(true); expect(pg[0].identity.threw).toBe(true);
expect(pg[0].identity.error).toMatch(/statsapi shape changed|roster down/); expect(pg[0].identity.error).toMatch(/statsapi shape changed/);
expect(pg[0].outcome).toBe(acq.PREGRADE_OUTCOME.IDENTITY_STAGE_ERROR); expect(pg[0].outcome).toBe(acq.PREGRADE_OUTCOME.EVENT_IDENTITY_STAGE_ERROR);
}); });
test('IDENTITY_STAGE_ERROR outranks everything downstream', async () => { test('EVENT_IDENTITY_STAGE_ERROR outranks everything downstream', async () => {
const t = acq.beginPregrade({ attemptId: 'a', sport: 'mlb', propsCount: 3 }); const t = acq.beginPregrade({ attemptId: 'a', sport: 'mlb', propsCount: 3 });
acq.pregradeRecorder(t).identity({ started: true, completed: false, threw: true, error: 'statsapi down' }); acq.pregradeRecorder(t).identity({ started: true, completed: false, threw: true, error: 'statsapi down' });
expect(acq.finishPregrade(t, {}).outcome).toBe(acq.PREGRADE_OUTCOME.IDENTITY_STAGE_ERROR); expect(acq.finishPregrade(t, {}).outcome).toBe(acq.PREGRADE_OUTCOME.EVENT_IDENTITY_STAGE_ERROR);
}); });
test('an empty collector alone can NEVER be classified', () => { test('an empty collector alone can NEVER be classified', () => {
@@ -225,7 +232,12 @@ describe('THE OBSERVER DOES NOT TOUCH THE PROPS', () => {
test('the recorder default is a frozen no-op, so existing callers are identical', () => { test('the recorder default is a frozen no-op, so existing callers are identical', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/services/gradeSlateService.js'), 'utf8'); const src = fs.readFileSync(path.join(ROOT, 'src/services/gradeSlateService.js'), 'utf8');
expect(src).toMatch(/const NO_PREGRADE = Object\.freeze\(\{ identity\(\) \{\}, admission\(\) \{\}, dedupe\(\) \{\}, gradeLoop\(\) \{\} \}\)/); // Now multi-line and covering the full recorder surface; match the shape,
// not the exact one-line form.
expect(src).toMatch(/const NO_PREGRADE = Object\.freeze\(\{/);
for (const m of ['identity', 'admission', 'dedupe', 'gradeLoop', 'gradeStarted', 'firstOnGraded']) {
expect(src).toMatch(new RegExp(`${m}\\(\\) \\{\\}`));
}
expect(src).toMatch(/const pregrade = opts\.pregrade \|\| NO_PREGRADE;/); expect(src).toMatch(/const pregrade = opts\.pregrade \|\| NO_PREGRADE;/);
}); });
}); });
@@ -309,7 +321,7 @@ describe('THE TRACE IS ACTUALLY PERSISTED BY THE REAL PATH', () => {
expect(pgStored[0].snapshot_attempt_id).toBe(acqStored[0].snapshot_attempt_id); expect(pgStored[0].snapshot_attempt_id).toBe(acqStored[0].snapshot_attempt_id);
expect(pgStored[0].input_props_count).toBe(3); expect(pgStored[0].input_props_count).toBe(3);
expect(pgStored[0].admission.input_count).toBe(3); expect(pgStored[0].admission.input_count).toBe(3);
expect(pgStored[0].outcome).toBe(acq.PREGRADE_OUTCOME.READY_FOR_GRADING); expect(pgStored[0].outcome).toBe(acq.PREGRADE_OUTCOME.GRADE_LOOP_STARTED);
}); });
}); });
+3 -1
View File
@@ -106,7 +106,9 @@ describe('CANONICAL EVENT AVAILABILITY BY ROW CLASS', () => {
// wraps it in try/catch to record and rethrow, which changed `const gate =` // wraps it in try/catch to record and rethrow, which changed `const gate =`
// into `gate = `. The invariant is that admission runs on (props, sport). // into `gate = `. The invariant is that admission runs on (props, sport).
expect(gs).toMatch(/gate = admitForGrading\(props, sport\)/); expect(gs).toMatch(/gate = admitForGrading\(props, sport\)/);
expect(gs).toMatch(/dedupeProps\(gate\.admitted, limit\)/); // The call gained an optional diagnostic stats object; match the call, not
// its exact arity.
expect(gs).toMatch(/dedupeProps\(gate\.admitted, limit/);
expect(gs).not.toMatch(/dedupeProps\(gate\.rejected/); expect(gs).not.toMatch(/dedupeProps\(gate\.rejected/);
}); });