Files
vyndr/tests/unit/acquisitionTrace.test.js
T
builtbykev f54b0627e1 Truthful provenance for an operator-invoked snapshot
The internal one-shot route already called the SAME production runSnapshot with
the SAME default dependencies — but it passed no trigger, and runSnapshot
defaults an absent trigger to SCHEDULED. So every operator-invoked run was
recorded as though the cron had fired it. That was a lie about provenance,
present by omission, and it would have contaminated the trace of any forced
diagnostic run.

CONTROLLED_FORCED is now its own trigger. Both internal routes (`/snapshot/:sport`
and `/snapshot/all`) stamp it, along with the process generation. The scheduler
still stamps SCHEDULED, and a test asserts neither internal route can label
itself scheduled.

Trace retention moves from "scheduled only" to a named allow-list of SCHEDULED +
CONTROLLED_FORCED. INTRADAY is still refused — it runs every ~20 minutes and
would displace scheduled evidence, which is the failure the store exists to
prevent. MANUAL_API stays refused too.

THE PIPELINE IS UNTOUCHED. snapshotService, gradeSlateService, retentionService,
snapshotScheduler, oddsService and eventIdentity are all UNCHANGED. A test
asserts the route injects no dependency override — no getOdds, gradeAndCacheSlate,
retention, ledger, cacheSet/cacheGet, gameBinder, eventIdentity, mlbAdapter or
notify — so the only difference from a scheduled invocation is the label and the
absence of a scheduled hour, which a forced run genuinely does not have.

The ?limit bisect-hook invariant is preserved and tightened: the opts passed
carry exactly {trigger, processStartedAt} and never a stray limit.

Teeth, injections verified present, against a green baseline:
  :sport route mislabelled SCHEDULED -> 3 fail
  /all route mislabelled SCHEDULED   -> 2 fail
  intraday admitted to the store     -> 5 fail
  trigger filter removed             -> 3 fail

THE FIRST TEETH RUN WAS INVALID AND IS DISCARDED: both routes live in one file,
so a single-occurrence replace hit `/snapshot/all` and left `/snapshot/:sport`
correct — the injection landed on the wrong target and the suite passed. Coverage
for `/all` was added, plus a test that the file contains exactly two
CONTROLLED_FORCED stamps and zero SCHEDULED ones, then both were re-run failing
independently.

Four stale assertions updated with the reason recorded: three pinned the
`not_scheduled` refusal string (now trigger-agnostic) and one pinned an empty
opts object on the route.

389 suites / 5,280 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
2026-08-28 02:56:22 -04:00

392 lines
19 KiB
JavaScript

'use strict';
/**
* SCHEDULED ACQUISITION OBSERVABILITY.
*
* The MLB snapshot stopped producing anything at the 22:00 and 01:00 slots on
* 2026-08-27 while NBA/WNBA ran normally. The differential narrowed it to two
* `runSnapshot` exits — `getOdds` THREW, or it RETURNED ZERO PROPS — and
* production retained nothing that could tell them apart: a failed acquisition
* has no snapshot_id, writes no ledger row and updates no slate.
*
* These tests prove the trace records the decision chain the existing control
* flow already makes, and that it changes nothing about acquisition.
*/
const fs = require('fs');
const path = require('path');
const acq = require('../../src/services/ops/acquisitionTrace');
const snap = require('../../src/services/snapshotService');
const ROOT = path.resolve(__dirname, '..', '..');
jest.setTimeout(15000);
// Deps that stop runSnapshot at the acquisition boundary: no grading, no
// retention, no network. The trace is finalized BEFORE any of this, so these
// only keep the test from running the whole pipeline.
const HALT = {
gradeAndCacheSlate: async () => ({ written: false, count: 0 }),
retention: null,
ledger: { recordPipelineGrades: async () => {}, captureClosing: async () => {}, gameDateFor: () => '2026-08-28' },
captureBookPrices: async () => {},
buildEspnIndex: async () => ({}),
};
const baseDeps = (over = {}) => ({
notify: async () => {}, sleep: async () => {}, retryDelayMs: 0,
now: () => '2026-08-28T03:00:00.000Z',
scheduledHourUtc: 3, processStartedAt: '2026-08-28T00:31:17.657Z',
retention: require('../../src/services/retentionService'),
cacheGet: async () => null, cacheSet: async () => {},
...over,
});
/** Run runSnapshot and return the trace it persisted (if any). */
async function runWithTrace(sport, getOdds, over = {}) {
const stored = [];
const result = await snap.runSnapshot(sport, baseDeps({
getOdds, persistAcquisitionTrace: async (t) => { stored.push(t); return { stored: true }; }, ...over,
}));
return { result, trace: stored[0] || null, stored };
}
const threw = (msg, extra = {}) => async () => { const e = new Error(msg); Object.assign(e, extra); throw e; };
const returns = (props, o = {}) => async () => ({ sport: 'mlb', props, provider: 'propline', source: 'live', ...o });
describe('ATTEMPT IDENTITY', () => {
test('every attempt gets a unique diagnostic id, distinct from every other identity', () => {
const a = acq.newAttemptId(); const b = acq.newAttemptId();
expect(a).not.toBe(b);
expect(a.startsWith('acq_')).toBe(true);
});
test('the trace carries sport, trigger, slot, start, code_sha and process generation', async () => {
const { trace } = await runWithTrace('mlb', returns([]));
expect(trace.sport).toBe('mlb');
expect(trace.trigger).toBe(acq.TRIGGER.SCHEDULED);
expect(trace.scheduled_hour_utc).toBe(3);
expect(trace.started_at).toBe('2026-08-28T03:00:00.000Z');
expect(trace).toHaveProperty('code_sha');
expect(trace.process_generation).toBe('2026-08-28T00:31:17.657Z');
});
test('it does NOT reuse or alter any product identity', () => {
const t = acq.begin({ sport: 'mlb' });
for (const forbidden of ['snapshot_id', 'read_id', 'claim_digest', 'canonical_event_id', 'ledger_id', 'publication_id']) {
expect(t).not.toHaveProperty(forbidden);
}
});
});
describe('THE TEST MATRIX — one scheduled acquisition', () => {
test('PRIMARY NONZERO -> runSnapshot continues, no fallback invented', async () => {
// wnba: no MLB event-identity branch, so this stops at the acquisition
// boundary without reaching statsapi.
const { result, trace } = await runWithTrace('wnba', returns([{ player: 'x' }, { player: 'y' }]), HALT);
expect(trace.final).toBe(acq.FINAL.NONZERO);
expect(trace.final_props_count).toBe(2);
expect(trace.outcome).toBe(acq.OUTCOME.CONTINUED);
expect(trace.attempts).toHaveLength(1);
expect(result.status).not.toBe('error');
expect(trace.sport).toBe('wnba');
});
test('FINAL ZERO -> EARLY_RETURN_ZERO_PROPS, recorded distinctly from an error', async () => {
const { result, trace } = await runWithTrace('mlb', returns([]));
expect(trace.final).toBe(acq.FINAL.ZERO);
expect(trace.final_props_count).toBe(0);
expect(trace.outcome).toBe(acq.OUTCOME.EARLY_RETURN_ZERO_PROPS);
expect(trace.final).not.toBe(acq.FINAL.THREW);
expect(result.status).toBe('skipped');
expect(result.reason).toBe('no props');
});
test('FINAL THROW -> EARLY_RETURN_ODDS_ERROR, with BOTH attempts preserved', async () => {
const { result, trace } = await runWithTrace('mlb', threw('Odds data temporarily unavailable.', { statusCode: 429 }));
expect(trace.final).toBe(acq.FINAL.THREW);
expect(trace.outcome).toBe(acq.OUTCOME.EARLY_RETURN_ODDS_ERROR);
// The EXISTING retry — primary + retry, not an added one.
expect(trace.attempts).toHaveLength(2);
expect(trace.attempts[0].is_retry).toBe(false);
expect(trace.attempts[1].is_retry).toBe(true);
expect(trace.attempts.every((a) => a.result.result === 'THREW')).toBe(true);
expect(result.status).toBe('error');
});
test('PRIMARY ERROR -> EXISTING RETRY SUCCESS: both attempts preserved, final NONZERO', async () => {
let n = 0;
const getOdds = async () => {
n += 1;
if (n === 1) throw new Error('transient');
return { sport: 'wnba', props: [{ player: 'x' }], provider: 'propline', source: 'live' };
};
const { trace } = await runWithTrace('wnba', getOdds, HALT);
expect(trace.attempts).toHaveLength(2);
expect(trace.attempts[0].result.result).toBe('THREW');
expect(trace.attempts[1].result.result).toBe('RETURNED');
expect(trace.final).toBe(acq.FINAL.NONZERO);
expect(trace.outcome).toBe(acq.OUTCOME.CONTINUED);
});
test('the recorded retry delay is the CONFIGURED one, never an added retry', async () => {
const { trace } = await runWithTrace('mlb', threw('x'), { retryDelayMs: 60000 });
expect(trace.attempts[1].configured_delay_ms).toBe(60000);
const src = fs.readFileSync(path.join(ROOT, 'src/services/snapshotService.js'), 'utf8');
// Exactly two getOdds calls and exactly one retry sleep — unchanged.
expect((src.match(/deps\.getOdds\(/g) || [])).toHaveLength(2);
expect((src.match(/deps\.sleep\(deps\.retryDelayMs\)/g) || [])).toHaveLength(1);
});
});
describe('THE DECISION CHAIN INSIDE getOdds', () => {
const chain = () => {
const t = acq.begin({ sport: 'mlb' });
acq.beginAttempt(t, { index: 0 });
return { t, rec: acq.recorder(t) };
};
test('PRIMARY ZERO -> RETRY ZERO -> FALLBACK BLOCKED records the exact chain', () => {
const { t, rec } = chain();
rec.cache({ considered: true, hit: false, decision: 'miss' });
rec.primary({ provider: 'propline', attempted: true, outcome: 'ZERO', count: 0 });
rec.fallback({
provider: 'odds-api', considered: true, allowed_at_invocation: false,
blocked_reason: 'quota_ceiling',
quota_at_invocation: { used: 478, limit: 500, pct: 0.956, period: '2026-08' },
attempted: false,
});
acq.finishAttempt(t, { result: 'THREW', error: 'Odds data temporarily unavailable.' });
const a = t.attempts[0];
expect(a.cache.hit).toBe(false);
expect(a.primary.outcome).toBe('ZERO');
expect(a.primary.count).toBe(0);
expect(a.fallback.allowed_at_invocation).toBe(false);
expect(a.fallback.blocked_reason).toBe('quota_ceiling');
expect(a.fallback.quota_at_invocation.used).toBe(478);
expect(a.fallback.attempted).toBe(false);
});
test('PRIMARY ERROR is recorded distinctly from PRIMARY ZERO', () => {
const { t, rec } = chain();
rec.primary({ provider: 'propline', attempted: true, outcome: 'ERROR', count: null, error: 'socket hang up' });
expect(t.attempts[0].primary.outcome).toBe('ERROR');
expect(t.attempts[0].primary.outcome).not.toBe('ZERO');
expect(t.attempts[0].primary.count).toBeNull();
});
test('FALLBACK SUCCESS is recorded when the existing behaviour reaches it', () => {
const { t, rec } = chain();
rec.primary({ provider: 'propline', attempted: true, outcome: 'ZERO', count: 0 });
rec.fallback({ provider: 'odds-api', considered: true, allowed_at_invocation: true, attempted: true, outcome: 'NONZERO', count: 120 });
expect(t.attempts[0].fallback.outcome).toBe('NONZERO');
expect(t.attempts[0].fallback.count).toBe(120);
});
test('quota is captured AT INVOCATION, inside getOdds, not read later', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/services/oddsService.js'), 'utf8');
const i = src.indexOf("const quotaStatus = await quotaTracker.getQuotaStatus('odds-api');");
expect(i).toBeGreaterThan(-1);
const block = src.slice(i, i + 900);
expect(block).toMatch(/quota_at_invocation/);
expect(block).toMatch(/allowed_at_invocation: !!quotaStatus\.allowed/);
});
test('the recorder is a pure write — a no-op recorder leaves behaviour identical', async () => {
const src = fs.readFileSync(path.join(ROOT, 'src/services/oddsService.js'), 'utf8');
expect(src).toMatch(/const NO_TRACE = Object\.freeze\(\{ cache\(\) \{\}, primary\(\) \{\}, fallback\(\) \{\} \}\)/);
expect(src).toMatch(/const trace = \(opts && opts\.trace\) \|\| NO_TRACE;/);
// No trace call may appear inside a conditional test.
expect(src).not.toMatch(/if\s*\([^)]*trace\.[a-z]/);
});
});
describe('THE OBSERVER MAKES NO PROVIDER CALL', () => {
const oddsSrc = fs.readFileSync(path.join(ROOT, 'src/services/oddsService.js'), 'utf8');
const traceSrc = fs.readFileSync(path.join(ROOT, 'src/services/ops/acquisitionTrace.js'), 'utf8');
test('the trace module contains no HTTP or provider call whatsoever', () => {
// Scan CODE, not prose or the sanitizer's own URL regex — a guard that
// flags its own redaction pattern is checking the wrong thing.
const code = traceSrc
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/(^|[^:])\/\/.*$/gm, '$1')
.replace(/s\.replace\([\s\S]*?\);/g, '');
for (const forbidden of ['fetch(', 'axios', 'getProps', 'fetchAllOdds', 'gateway',
"require('http", 'https://', 'getOdds', 'oddsService', 'proplineAdapter']) {
expect(code).not.toContain(forbidden);
}
});
test('instrumenting getOdds added no provider request', () => {
expect((oddsSrc.match(/await propline\.getProps\(/g) || [])).toHaveLength(1);
expect((oddsSrc.match(/await fetchAllOdds\(/g) || [])).toHaveLength(1);
});
test('the internal read endpoint runs no pipeline and no fetch', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/routes/internal.js'), 'utf8');
const i = src.indexOf("router.get('/acquisition/:sport'");
expect(i).toBeGreaterThan(-1);
const block = src.slice(i, src.indexOf('});', src.indexOf('} catch', i)));
expect(block).not.toMatch(/getOdds|runSnapshot|diagnose|fetch\(/);
expect(block).toMatch(/acq\.history/);
});
});
describe('SANITIZATION', () => {
test('credentials, tokens and URLs never reach the trace', () => {
const dirty = 'failed https://api.propline.io/v1/odds?apiKey=abcd1234abcd1234abcd1234 token=zzzzzzzzzzzzzzzzzzzzzzzzzz';
const clean = acq.sanitize(dirty);
expect(clean).not.toMatch(/abcd1234abcd1234/);
expect(clean).not.toMatch(/api\.propline\.io/);
expect(clean).not.toMatch(/zzzzzzzzzzzzzzzzzzzzzz/);
expect(clean).toMatch(/\[url\]|\[redacted\]/);
});
test('a thrown provider error is sanitized on the way into the trace', async () => {
const { trace } = await runWithTrace('mlb', threw('boom https://x.io/y?key=SUPERSECRETKEYVALUE123456'));
const blob = JSON.stringify(trace);
expect(blob).not.toMatch(/SUPERSECRETKEYVALUE/);
expect(blob).not.toMatch(/x\.io/);
});
test('no prop payload is retained — counts only', async () => {
const { trace } = await runWithTrace('mlb', returns([{ player: 'Aaron Judge', stat: 'hits' }]));
expect(JSON.stringify(trace)).not.toMatch(/Aaron Judge/);
expect(trace.final_props_count).toBe(1);
});
});
describe('HISTORY IS BOUNDED, PER SPORT, AND SCHEDULED-ONLY', () => {
test('storage is a bounded per-sport list with a TTL', () => {
expect(acq.key('mlb')).toBe('ops:acquisition:mlb');
expect(acq.key('wnba')).not.toBe(acq.key('mlb'));
expect(acq.HISTORY_CAP).toBeGreaterThan(1);
expect(acq.HISTORY_CAP).toBeLessThanOrEqual(20);
expect(acq.TTL_SECONDS).toBeGreaterThanOrEqual(86400);
});
test('it uses ATOMIC append (lpush/ltrim), so two schedulers cannot overwrite each other', async () => {
const calls = [];
const client = {
lpush: async (k, v) => { calls.push(['lpush', k, JSON.parse(v).snapshot_attempt_id]); },
ltrim: async (k, a, b) => calls.push(['ltrim', k, a, b]),
expire: async (k, t) => calls.push(['expire', k, t]),
};
const a = acq.finish(acq.begin({ sport: 'mlb' }), { final: acq.FINAL.THREW });
const b = acq.finish(acq.begin({ sport: 'mlb' }), { final: acq.FINAL.THREW });
await acq.persist(a, { getRedisClient: () => client });
await acq.persist(b, { getRedisClient: () => client });
expect(calls.filter((c) => c[0] === 'lpush')).toHaveLength(2);
expect(calls.filter((c) => c[0] === 'ltrim')).toHaveLength(2);
// Read-modify-write would show a get; there is none.
expect(calls.some((c) => c[0] === 'get' || c[0] === 'set')).toBe(false);
const src = fs.readFileSync(path.join(ROOT, 'src/services/ops/acquisitionTrace.js'), 'utf8');
expect(src).toMatch(/client\.lpush/);
expect(src).not.toMatch(/cacheSet\(/);
});
test('INTRADAY SUCCESS CANNOT OVERWRITE A SCHEDULED FAILURE', async () => {
// Structural: intraday never calls runSnapshot, and persist refuses any
// non-scheduled trigger outright.
const intraday = acq.finish(acq.begin({ sport: 'mlb', trigger: acq.TRIGGER.INTRADAY }), { final: acq.FINAL.NONZERO });
const client = { lpush: async () => { throw new Error('must not be called'); }, ltrim: async () => {}, expire: async () => {} };
const out = await acq.persist(intraday, { getRedisClient: () => client });
expect(out.stored).toBe(false);
expect(out.reason).toBe('not_retained_trigger');
const src = fs.readFileSync(path.join(ROOT, 'src/services/intradayRefreshService.js'), 'utf8');
expect(src).not.toMatch(/runSnapshot/);
});
test('ANOTHER SPORT CANNOT OVERWRITE MLB EVIDENCE', async () => {
const keys = [];
const client = { lpush: async (k) => keys.push(k), ltrim: async () => {}, expire: async () => {} };
for (const sp of ['mlb', 'nba', 'wnba']) {
await acq.persist(acq.finish(acq.begin({ sport: sp }), { final: acq.FINAL.ZERO }), { getRedisClient: () => client });
}
expect(new Set(keys).size).toBe(3);
expect(keys).toContain('ops:acquisition:mlb');
});
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.begin({ sport: 'mlb', scheduledHourUtc: 3, processStartedAt: gen });
await acq.persist(acq.finish(t, { final: acq.FINAL.ZERO }), { getRedisClient: () => client });
}
expect(stored).toHaveLength(2);
expect(stored[0].snapshot_attempt_id).not.toBe(stored[1].snapshot_attempt_id);
expect(new Set(stored.map((t) => t.process_generation)).size).toBe(2);
});
});
describe('TELEMETRY FAILURE MUST NOT BREAK THE PRODUCT', () => {
test('a trace-store failure does not fail a healthy acquisition', async () => {
const { result, stored } = await runWithTrace('wnba', returns([{ p: 1 }]), {
...HALT,
persistAcquisitionTrace: async () => { throw new Error('redis down'); },
});
// The snapshot continued past acquisition; it did not return an odds error.
expect(result.status).not.toBe('error');
expect(stored).toHaveLength(0);
});
test('the default path never constructs a redis client under test', async () => {
// The opsNotify precedent. Without it, every suite driving runSnapshot
// would open a real connection and block.
const out = await acq.persist(acq.finish(acq.begin({ sport: 'mlb' }), { final: acq.FINAL.ZERO }));
expect(out.stored).toBe(false);
expect(out.reason).toBe('test_env');
});
test('persist swallows a store error and reports it rather than throwing', async () => {
const client = { lpush: async () => { throw new Error('redis down'); }, ltrim: async () => {}, expire: async () => {} };
const out = await acq.persist(acq.finish(acq.begin({ sport: 'mlb' }), { final: acq.FINAL.ZERO }), { getRedisClient: () => client });
expect(out.stored).toBe(false);
expect(out.reason).toMatch(/redis down/);
});
test('the default persist dep is wrapped so it can never throw into runSnapshot', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/services/snapshotService.js'), 'utf8');
const i = src.indexOf('persistAcquisitionTrace: opts.persistAcquisitionTrace');
expect(i).toBeGreaterThan(-1);
expect(src.slice(i, i + 220)).toMatch(/try \{[\s\S]*catch/);
});
});
describe('ACQUISITION BEHAVIOUR IS UNCHANGED', () => {
test('getOdds gained only an optional recorder argument', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/services/oddsService.js'), 'utf8');
expect(src).toMatch(/async function getOdds\(sport, opts = \{\}\)/);
});
test('provider order, quota threshold and cache policy are untouched', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/services/oddsService.js'), 'utf8');
// PropLine still first, still gated on hasKeys, still the >0 test.
expect(src).toMatch(/if \(propline\.hasKeys\(\)\) \{/);
expect(src).toMatch(/pl\.props\.length > 0/);
expect(src).toMatch(/if \(!quotaStatus\.allowed\) \{/);
});
test('runSnapshot HONOURS the caller trigger — it is not hardcoded scheduled', async () => {
// Without this, a non-scheduled acquisition would be stamped SCHEDULED and
// could displace real scheduled evidence.
const { trace } = await runWithTrace('mlb', returns([]), { trigger: acq.TRIGGER.INTRADAY });
expect(trace.trigger).toBe(acq.TRIGGER.INTRADAY);
expect(trace.trigger).not.toBe(acq.TRIGGER.SCHEDULED);
const client = { lpush: async () => { throw new Error('must not be called'); }, ltrim: async () => {}, expire: async () => {} };
expect((await acq.persist(trace, { getRedisClient: () => client })).reason).toBe('not_retained_trigger');
});
test('the scheduler passes only diagnostic context, not behaviour', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/snapshotScheduler.js'), 'utf8');
const i = src.indexOf('const results = await runAll({');
const block = src.slice(i, i + 320);
expect(block).toMatch(/sports: scheduled/);
expect(block).toMatch(/trigger: acq\.TRIGGER\.SCHEDULED/);
expect(block).toMatch(/scheduledHourUtc: h/);
expect(block).toMatch(/processStartedAt: PROCESS_STARTED_AT/);
expect(block).not.toMatch(/retryDelayMs|getOdds|quota/);
});
});