'use strict'; /** * PRE-GRADING COHORT CONSTRUCTION — the stage between acquisition and grading. * * The acquisition recorder proved the earlier diagnosis wrong: the 03:00 MLB * attempt acquired 6,365 props and CONTINUED. Zero reached the first grading * callback, and nothing durable said why. * * The region is small and every branch in it returns the SAME * `{written:false,count:0}`: * admitForGrading (pure, can throw -> swallowed by gradeAndCacheSlate) * dedupeProps (pure, can throw -> swallowed; ALSO FILTERS by model book) * mapLimit(gradeBestSide) <- the first onGraded-capable call * * So an exception, a mass rejection and a dedupe-to-zero are indistinguishable * from outside. `ALL_REJECTED` must never be inferred from an empty collector. */ const fs = require('fs'); const path = require('path'); const acq = require('../../src/services/ops/acquisitionTrace'); const gs = require('../../src/services/gradeSlateService'); const { MODEL_BOOKS } = require('../../src/config/bookRoles'); const ROOT = path.resolve(__dirname, '..', '..'); const BOOK = [...MODEL_BOOKS][0]; const props = (n, over = {}) => Array.from({ length: n }, (_, i) => ({ player: `Player ${i}`, stat_type: 'hits', line: 0.5 + i, sport: 'mlb', book: BOOK, home_team: 'Reds', away_team: 'Cardinals', ...over, })); async function runStage(input, opts = {}) { const t = acq.beginPregrade({ attemptId: 'acq_test', sport: 'mlb', propsCount: input.length }); const res = await gs.gradeAndCacheSlate('mlb', input, { pregrade: acq.pregradeRecorder(t), cacheSet: async () => {}, grade: async () => null, ...opts, }); return { trace: acq.finishPregrade(t, {}), res }; } describe('THE STAGE MATRIX', () => { test('READY_FOR_GRADING — identity + admission + dedupe all succeed', async () => { const { trace } = await runStage(props(5)); expect(trace.admission.completed).toBe(true); expect(trace.admission.admitted_count).toBe(5); expect(trace.admission.rejected_count).toBe(0); expect(trace.dedupe.completed).toBe(true); expect(trace.dedupe.output_count).toBe(5); expect(trace.grade_loop).toMatchObject({ candidate_count: 5, reached: true }); // 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 () => { const { trace } = await runStage(props(6, { event_binding_status: 'UNRESOLVED' })); expect(trace.admission.admitted_count).toBe(0); expect(trace.admission.rejected_count).toBe(6); // admitted + rejected must reconcile to the admission input exactly. expect(trace.admission.admitted_count + trace.admission.rejected_count) .toBe(trace.admission.input_count); const reasons = trace.admission.rejection_reason_counts; expect(Object.values(reasons).reduce((a, b) => a + b, 0)).toBe(6); expect(reasons.EVENT_UNRESOLVED).toBe(6); expect(trace.outcome).toBe(acq.PREGRADE_OUTCOME.ALL_REJECTED); }); test('MIXED admission reconciles admitted + rejected', async () => { const { trace } = await runStage([ ...props(4), ...props(3, { event_binding_status: 'CONTRADICTED' }), ]); expect(trace.admission.input_count).toBe(7); expect(trace.admission.admitted_count).toBe(4); expect(trace.admission.rejected_count).toBe(3); expect(trace.admission.rejection_reason_counts.EVENT_PLAYER_TEAM_CONTRADICTION).toBe(3); expect(trace.outcome).toBe(acq.PREGRADE_OUTCOME.GRADE_LOOP_STARTED); }); test('DEDUPE_EMPTY — admitted > 0 but dedupe filters to zero, NOT ALL_REJECTED', async () => { // dedupeProps FILTERS by model book. A fully-admitted set can still reach // the grader with nothing, and that is a different fact from a rejection. const { trace } = await runStage(props(5, { book: 'not_a_model_book' })); expect(trace.admission.admitted_count).toBe(5); expect(trace.dedupe.input_count).toBe(5); expect(trace.dedupe.output_count).toBe(0); // 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); }); test('ADMISSION_STAGE_ERROR — a throw is never folded into ALL_REJECTED', async () => { const t = acq.beginPregrade({ attemptId: 'a', sport: 'mlb', propsCount: 3 }); const rec = acq.pregradeRecorder(t); rec.admission({ started: true, completed: false, threw: true, input_count: 3, error: 'boom' }); expect(acq.finishPregrade(t, {}).outcome).toBe(acq.PREGRADE_OUTCOME.ADMISSION_STAGE_ERROR); expect(t.outcome).not.toBe(acq.PREGRADE_OUTCOME.ALL_REJECTED); }); test('DEDUPE_STAGE_ERROR — a dedupe throw is not reported as output zero', async () => { const t = acq.beginPregrade({ attemptId: 'a', sport: 'mlb', propsCount: 3 }); const rec = acq.pregradeRecorder(t); rec.admission({ started: true, completed: true, admitted_count: 3, rejected_count: 0, input_count: 3 }); rec.dedupe({ started: true, completed: false, threw: true, input_count: 3, error: 'boom' }); expect(acq.finishPregrade(t, {}).outcome).toBe(acq.PREGRADE_OUTCOME.DEDUPE_STAGE_ERROR); }); test('a REAL dedupe throw is recorded as DEDUPE_STAGE_ERROR, not output zero', async () => { // Drive the real gradeAndCacheSlate: admitted passes, then dedupeProps // throws while reading the prop's book. const bad = props(2); Object.defineProperty(bad[1], 'book', { enumerable: true, get() { throw new Error('book poison'); } }); const { res, trace } = await runStage(bad); expect(res.written).toBe(false); expect(trace.admission.completed).toBe(true); expect(trace.admission.admitted_count).toBe(2); expect(trace.dedupe.threw).toBe(true); expect(trace.dedupe).not.toHaveProperty('output_count'); expect(trace.outcome).toBe(acq.PREGRADE_OUTCOME.DEDUPE_STAGE_ERROR); expect(trace.outcome).not.toBe(acq.PREGRADE_OUTCOME.DEDUPE_EMPTY); }); test('a REAL identity throw reaches the trace through runSnapshot', async () => { // The mlb identity block has its own catch. Recording must survive it. const snap2 = require('../../src/services/snapshotService'); const pg = []; await snap2.runSnapshot('mlb', { getOdds: async () => ({ sport: 'mlb', props: props(2), provider: 'propline', source: 'cache' }), gradeAndCacheSlate: async () => ({ written: false, count: 0 }), // 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, attachEventIdentity: () => { throw new Error('statsapi shape changed'); } }, mlbAdapter: { getScheduleWithPitchers: async () => [] }, notify: async () => {}, sleep: async () => {}, retryDelayMs: 0, now: () => '2026-08-28T14:00:00.000Z', scheduledHourUtc: 14, processStartedAt: 'p', retention: null, ledger: { recordPipelineGrades: async () => {}, captureClosing: async () => {}, gameDateFor: () => '2026-08-28' }, captureBookPrices: async () => ({}), buildEspnIndex: async () => ({}), cacheGet: async () => null, cacheSet: async () => {}, persistAcquisitionTrace: async () => {}, persistPregradeTrace: async (t) => { pg.push(t); }, }); expect(pg).toHaveLength(1); expect(pg[0].identity).toBeTruthy(); expect(pg[0].identity.threw).toBe(true); expect(pg[0].identity.error).toMatch(/statsapi shape changed/); expect(pg[0].outcome).toBe(acq.PREGRADE_OUTCOME.EVENT_IDENTITY_STAGE_ERROR); }); test('EVENT_IDENTITY_STAGE_ERROR outranks everything downstream', async () => { const t = acq.beginPregrade({ attemptId: 'a', sport: 'mlb', propsCount: 3 }); acq.pregradeRecorder(t).identity({ started: true, completed: false, threw: true, error: 'statsapi down' }); expect(acq.finishPregrade(t, {}).outcome).toBe(acq.PREGRADE_OUTCOME.EVENT_IDENTITY_STAGE_ERROR); }); test('an empty collector alone can NEVER be classified', () => { // The whole point: with no admission evidence the classifier refuses to say // ALL_REJECTED. This is the defect the previous tranche talked itself into. const t = acq.beginPregrade({ attemptId: 'a', sport: 'mlb', propsCount: 6365 }); expect(acq.finishPregrade(t, {}).outcome).toBe(acq.PREGRADE_OUTCOME.OTHER_PREGRADING_ERROR); expect(t.outcome).not.toBe(acq.PREGRADE_OUTCOME.ALL_REJECTED); }); }); describe('EXCEPTION SEMANTICS ARE UNCHANGED', () => { const src = fs.readFileSync(path.join(ROOT, 'src/services/gradeSlateService.js'), 'utf8'); test('each recorded stage RETHROWS the identical error', () => { expect(src).toMatch(/catch \(eAdm\) \{[\s\S]{0,220}throw eAdm;/); expect(src).toMatch(/catch \(eDed\) \{[\s\S]{0,220}throw eDed;/); }); test('the enclosing best-effort catch is still the only handler', () => { expect(src).toMatch(/\/\/ Best-effort — slate grading must never break odds delivery\./); expect(src).toMatch(/return \{ written: false, count: 0, error: e\.message \};/); }); test('a real admission throw still returns the legacy shape, not a crash', async () => { const boom = { admitForGrading: null }; void boom; // Drive the real function with a prop set that makes admitForGrading throw // by poisoning the array itself (a non-object entry is skipped, so use a // getter that throws on read). const bad = props(2); Object.defineProperty(bad, '2', { enumerable: true, get() { throw new Error('poison'); } }); bad.length = 3; const { res, trace } = await runStage(bad); expect(res.written).toBe(false); expect(res.error).toMatch(/poison/); expect(trace.outcome).toBe(acq.PREGRADE_OUTCOME.ADMISSION_STAGE_ERROR); }); }); describe('THE OBSERVER DOES NOT TOUCH THE PROPS', () => { test('prop objects and array order are unchanged', async () => { const input = props(4); const before = JSON.parse(JSON.stringify(input)); const order = input.map((p) => p.player); await runStage(input); expect(input.map((p) => p.player)).toEqual(order); expect(input).toHaveLength(4); expect(JSON.parse(JSON.stringify(input))).toEqual(before); }); test('the recorder writes only into the trace object', () => { const t = acq.beginPregrade({ attemptId: 'a', sport: 'mlb' }); const rec = acq.pregradeRecorder(t); const p = { player: 'x' }; rec.admission({ started: true, completed: true, admitted_count: 1, rejected_count: 0, input_count: 1 }); expect(Object.keys(p)).toEqual(['player']); expect(t.admission.admitted_count).toBe(1); }); test('the CANDIDATE array is never reordered or filtered by the observer', () => { // Reordering `unique` would change grading order and the first-row-wins cap // — a behaviour change wearing an observer's clothes. const src = fs.readFileSync(path.join(ROOT, 'src/services/gradeSlateService.js'), 'utf8'); const from = src.indexOf('let unique;'); const to = src.indexOf('const graded = (await mapLimit', from); expect(from).toBeGreaterThan(-1); expect(to).toBeGreaterThan(from); const region = src.slice(from, to); for (const mutator of ['.sort(', '.reverse(', '.splice(', '.filter(', '.push(', '.pop(', '.shift(']) { expect(region).not.toContain(mutator); } }); 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'); // 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;/); }); }); describe('IDENTITY CORRELATION AND STORAGE', () => { test('the pre-grading trace reuses the ACQUISITION attempt id', () => { const t = acq.beginPregrade({ attemptId: 'acq_b01f75cc', sport: 'mlb' }); expect(t.snapshot_attempt_id).toBe('acq_b01f75cc'); const src = fs.readFileSync(path.join(ROOT, 'src/services/snapshotService.js'), 'utf8'); expect(src).toMatch(/attemptId: acqTrace\.snapshot_attempt_id/); }); test('it carries no product identity', () => { const t = acq.beginPregrade({ attemptId: 'a', sport: 'mlb' }); for (const f of ['snapshot_id', 'read_id', 'claim_digest', 'canonical_event_id', 'publication_id']) { expect(t).not.toHaveProperty(f); } }); test('it stores under its OWN key, so it cannot displace the acquisition record', () => { expect(acq.pregradeKey('mlb')).toBe('ops:pregrade:mlb'); expect(acq.pregradeKey('mlb')).not.toBe(acq.key('mlb')); }); test('per sport, atomic append, scheduled-only', async () => { const keys = []; const client = { lpush: async (k, v) => keys.push([k, JSON.parse(v).snapshot_attempt_id]), ltrim: async () => {}, expire: async () => {} }; for (const sp of ['mlb', 'wnba']) { await acq.persistPregrade(acq.finishPregrade(acq.beginPregrade({ attemptId: `a_${sp}`, sport: sp }), {}), { getRedisClient: () => client }); } expect(keys.map((k) => k[0])).toEqual(['ops:pregrade:mlb', 'ops:pregrade:wnba']); const intraday = acq.beginPregrade({ attemptId: 'x', sport: 'mlb', trigger: acq.TRIGGER.INTRADAY }); const out = await acq.persistPregrade(acq.finishPregrade(intraday, {}), { getRedisClient: () => ({ lpush: async () => { throw new Error('must not be called'); }, ltrim: async () => {}, expire: async () => {} }), }); expect(out.reason).toBe('not_retained_trigger'); }); test('two schedulers on the same slot are retained separately', async () => { const stored = []; const client = { lpush: async (k, v) => stored.push(JSON.parse(v)), ltrim: async () => {}, expire: async () => {} }; for (const gen of ['proc-A', 'proc-B']) { const t = acq.beginPregrade({ attemptId: `acq_${gen}`, sport: 'mlb', processStartedAt: gen }); await acq.persistPregrade(acq.finishPregrade(t, {}), { getRedisClient: () => client }); } expect(new Set(stored.map((t) => t.process_generation)).size).toBe(2); }); test('no credential or raw payload reaches the trace', async () => { const t = acq.beginPregrade({ attemptId: 'a', sport: 'mlb' }); acq.pregradeRecorder(t).admission({ started: true, completed: false, threw: true, error: 'boom https://x.io/y?apiKey=SUPERSECRETVALUE1234567' }); const blob = JSON.stringify(t); expect(blob).not.toMatch(/SUPERSECRETVALUE/); expect(blob).not.toMatch(/x\.io/); }); }); describe('THE TRACE IS ACTUALLY PERSISTED BY THE REAL PATH', () => { // A recorder that is built, correct, and never invoked is the exact failure // this whole programme has hit before. Drive the REAL runSnapshot. const snap = require('../../src/services/snapshotService'); test('runSnapshot persists a pre-grading trace correlated to the acquisition id', async () => { const acqStored = []; const pgStored = []; await snap.runSnapshot('wnba', { getOdds: async () => ({ sport: 'wnba', props: props(3), provider: 'propline', source: 'cache' }), gradeAndCacheSlate: require('../../src/services/gradeSlateService').gradeAndCacheSlate, grade: async () => null, 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 () => ({}), cacheGet: async () => null, cacheSet: async () => {}, persistAcquisitionTrace: async (t) => { acqStored.push(t); }, persistPregradeTrace: async (t) => { pgStored.push(t); }, }); expect(acqStored).toHaveLength(1); expect(pgStored).toHaveLength(1); // SAME identity, not a second run id. expect(pgStored[0].snapshot_attempt_id).toBe(acqStored[0].snapshot_attempt_id); expect(pgStored[0].input_props_count).toBe(3); expect(pgStored[0].admission.input_count).toBe(3); expect(pgStored[0].outcome).toBe(acq.PREGRADE_OUTCOME.GRADE_LOOP_STARTED); }); }); describe('TELEMETRY IS BEST-EFFORT', () => { test('a trace-store failure does not change the product outcome', async () => { const client = { lpush: async () => { throw new Error('redis down'); }, ltrim: async () => {}, expire: async () => {} }; const out = await acq.persistPregrade(acq.finishPregrade(acq.beginPregrade({ attemptId: 'a', sport: 'mlb' }), {}), { getRedisClient: () => client }); expect(out.stored).toBe(false); expect(out.reason).toMatch(/redis down/); }); test('the call site guards the write so it can never throw into runSnapshot', () => { const src = fs.readFileSync(path.join(ROOT, 'src/services/snapshotService.js'), 'utf8'); const i = src.indexOf('const savePgTrace = async ()'); expect(i).toBeGreaterThan(-1); expect(src.slice(i, i + 320)).toMatch(/try \{[\s\S]*catch/); }); test('the default path opens no redis client under test', async () => { const out = await acq.persistPregrade(acq.finishPregrade(acq.beginPregrade({ attemptId: 'a', sport: 'mlb' }), {})); expect(out.reason).toBe('test_env'); }); });