f54b0627e1
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
392 lines
19 KiB
JavaScript
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/);
|
|
});
|
|
});
|