From f54b0627e1d8ecdc162b843d802463a1e1cc8076 Mon Sep 17 00:00:00 2001 From: Kev Date: Fri, 28 Aug 2026 02:56:22 -0400 Subject: [PATCH] Truthful provenance for an operator-invoked snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8 --- src/routes/internal.js | 18 ++- src/services/ops/acquisitionTrace.js | 19 +++- tests/integration/snapshotRoutes.test.js | 15 ++- tests/unit/acquisitionTrace.test.js | 4 +- tests/unit/controlledForcedTrigger.test.js | 123 +++++++++++++++++++++ tests/unit/pregradeTrace.test.js | 2 +- 6 files changed, 169 insertions(+), 12 deletions(-) create mode 100644 tests/unit/controlledForcedTrigger.test.js diff --git a/src/routes/internal.js b/src/routes/internal.js index 6d44f12..fa1ec6f 100644 --- a/src/routes/internal.js +++ b/src/routes/internal.js @@ -180,7 +180,11 @@ router.post('/prefetch/tank01', async (req, res) => { router.post('/snapshot/all', async (req, res) => { const snapshot = require('../services/snapshotService'); try { - const results = await snapshot.runAllSnapshots(); + const acqT = require('../services/ops/acquisitionTrace'); + const results = await snapshot.runAllSnapshots({ + trigger: acqT.TRIGGER.CONTROLLED_FORCED, + processStartedAt: PROCESS_STARTED_AT, + }); return res.json({ ok: true, results }); } catch (err) { const message = err && err.message ? err.message : String(err); @@ -283,7 +287,17 @@ router.post('/snapshot/:sport', async (req, res) => { // slate for one run so a cap regression can be isolated by measurement // rather than guessed at. Omitted => gradeSlateService's real DEFAULT_LIMIT. const limit = Number(req.query.limit) > 0 ? Number(req.query.limit) : undefined; - const summary = await snapshot.runSnapshot(req.params.sport, limit ? { limit } : {}); + // TRUTHFUL PROVENANCE. runSnapshot defaults an absent trigger to SCHEDULED, + // so an operator-invoked run was previously recorded as though the cron + // fired it. It is now labelled for what it is. This changes ONLY the + // diagnostic trace — the pipeline path, the dependencies and every + // authoritative write are identical to the scheduled invocation. + const acqT = require('../services/ops/acquisitionTrace'); + const summary = await snapshot.runSnapshot(req.params.sport, { + ...(limit ? { limit } : {}), + trigger: acqT.TRIGGER.CONTROLLED_FORCED, + processStartedAt: PROCESS_STARTED_AT, + }); return res.json({ ok: true, ...(limit ? { limit_override: limit } : {}), summary }); } catch (err) { const message = err && err.message ? err.message : String(err); diff --git a/src/services/ops/acquisitionTrace.js b/src/services/ops/acquisitionTrace.js index 8ad9b68..d138ed4 100644 --- a/src/services/ops/acquisitionTrace.js +++ b/src/services/ops/acquisitionTrace.js @@ -39,10 +39,23 @@ const TTL_SECONDS = 172800; // 48h const TRIGGER = Object.freeze({ SCHEDULED: 'SCHEDULED_SNAPSHOT', + // An operator-invoked run of the SAME production snapshot path. Recorded + // under its own name because labelling it SCHEDULED would be a lie about + // provenance — and the internal route previously did exactly that by + // omission, since `deps.trigger || TRIGGER.SCHEDULED` defaults that way. + CONTROLLED_FORCED: 'CONTROLLED_FORCED', INTRADAY: 'INTRADAY_REFRESH', MANUAL: 'MANUAL_API', }); +/** + * Which triggers are retained. A controlled forced run is a legitimate one-shot + * diagnostic of the production path and is kept, tagged as itself. INTRADAY is + * still refused: it runs every ~20 minutes and would displace scheduled + * evidence, which is the failure this store exists to prevent. + */ +const RETAINED_TRIGGERS = Object.freeze([TRIGGER.SCHEDULED, TRIGGER.CONTROLLED_FORCED]); + const SOURCE_OUTCOME = Object.freeze({ NONZERO: 'NONZERO', ZERO: 'ZERO', @@ -202,7 +215,7 @@ async function persist(trace, deps = {}) { if (!trace || !trace.sport) return { stored: false, reason: 'no_sport' }; // Only SCHEDULED attempts are retained; an intraday success must not be able // to displace a scheduled failure. - if (trace.trigger !== TRIGGER.SCHEDULED) return { stored: false, reason: 'not_scheduled' }; + if (!RETAINED_TRIGGERS.includes(trace.trigger)) return { stored: false, reason: 'not_retained_trigger' }; return pushBounded(key(trace.sport), trace, deps); } @@ -389,7 +402,7 @@ function finishPregrade(trace, { now } = {}) { async function persistPregrade(trace, deps = {}) { if (!trace || !trace.sport) return { stored: false, reason: 'no_sport' }; - if (trace.trigger !== TRIGGER.SCHEDULED) return { stored: false, reason: 'not_scheduled' }; + if (!RETAINED_TRIGGERS.includes(trace.trigger)) return { stored: false, reason: 'not_retained_trigger' }; return pushBounded(pregradeKey(trace.sport), trace, deps); } @@ -399,7 +412,7 @@ async function pregradeHistory(sport, deps = {}) { module.exports = { - TRIGGER, SOURCE_OUTCOME, FINAL, OUTCOME, PREGRADE_OUTCOME, + TRIGGER, RETAINED_TRIGGERS, SOURCE_OUTCOME, FINAL, OUTCOME, PREGRADE_OUTCOME, pregradeKey, beginPregrade, pregradeRecorder, classifyPregrade, finishPregrade, reconcilePregrade, persistPregrade, pregradeHistory, diff --git a/tests/integration/snapshotRoutes.test.js b/tests/integration/snapshotRoutes.test.js index 1502def..69f6973 100644 --- a/tests/integration/snapshotRoutes.test.js +++ b/tests/integration/snapshotRoutes.test.js @@ -38,10 +38,17 @@ describe('POST /api/internal/snapshot/:sport', () => { .set('x-internal-key', 'test-key-123') .send({}); expect(res.status).toBe(200); - // Opts object added 2026-08-01 for the ?limit= bisect hook. With no - // ?limit the opts must be EMPTY — a stray limit here would silently cap - // production runs, which is the exact bug the hook exists to diagnose. - expect(snapshot.runSnapshot).toHaveBeenCalledWith('mlb', {}); + // Opts object added 2026-08-01 for the ?limit= bisect hook. With no ?limit + // there must be NO limit key — a stray one would silently cap production + // runs, which is the exact bug the hook exists to diagnose. The opts now + // also carry truthful provenance (an operator run is CONTROLLED_FORCED, not + // SCHEDULED); those two diagnostic keys are the only permitted additions. + expect(snapshot.runSnapshot).toHaveBeenCalledWith('mlb', expect.objectContaining({ + trigger: 'CONTROLLED_FORCED', + })); + const passedOpts = snapshot.runSnapshot.mock.calls[0][1]; + expect(Object.keys(passedOpts).sort()).toEqual(['processStartedAt', 'trigger']); + expect(passedOpts).not.toHaveProperty('limit'); expect(res.body.summary.gradeCount).toBe(3); }); diff --git a/tests/unit/acquisitionTrace.test.js b/tests/unit/acquisitionTrace.test.js index 746ced7..6a33f53 100644 --- a/tests/unit/acquisitionTrace.test.js +++ b/tests/unit/acquisitionTrace.test.js @@ -292,7 +292,7 @@ describe('HISTORY IS BOUNDED, PER SPORT, AND SCHEDULED-ONLY', () => { 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_scheduled'); + expect(out.reason).toBe('not_retained_trigger'); const src = fs.readFileSync(path.join(ROOT, 'src/services/intradayRefreshService.js'), 'utf8'); expect(src).not.toMatch(/runSnapshot/); }); @@ -375,7 +375,7 @@ describe('ACQUISITION BEHAVIOUR IS UNCHANGED', () => { 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_scheduled'); + expect((await acq.persist(trace, { getRedisClient: () => client })).reason).toBe('not_retained_trigger'); }); test('the scheduler passes only diagnostic context, not behaviour', () => { diff --git a/tests/unit/controlledForcedTrigger.test.js b/tests/unit/controlledForcedTrigger.test.js new file mode 100644 index 0000000..d836188 --- /dev/null +++ b/tests/unit/controlledForcedTrigger.test.js @@ -0,0 +1,123 @@ +'use strict'; + +/** + * CONTROLLED_FORCED provenance. + * + * `runSnapshot` defaults an absent trigger to SCHEDULED, so the internal + * one-shot route recorded operator-invoked runs as though the cron had fired + * them. That is a lie about provenance, and it was there by omission. + * + * The pipeline path is unchanged: the route already called the same production + * runSnapshot with the same default dependencies. Only the label is corrected, + * and the trace store now retains that label. + */ + +const fs = require('fs'); +const path = require('path'); +const acq = require('../../src/services/ops/acquisitionTrace'); +const ROOT = path.resolve(__dirname, '..', '..'); + +describe('THE TRIGGER IS TRUTHFUL', () => { + test('CONTROLLED_FORCED exists and is distinct from SCHEDULED', () => { + expect(acq.TRIGGER.CONTROLLED_FORCED).toBe('CONTROLLED_FORCED'); + expect(acq.TRIGGER.CONTROLLED_FORCED).not.toBe(acq.TRIGGER.SCHEDULED); + }); + + test('the internal one-shot route labels its runs CONTROLLED_FORCED', () => { + const src = fs.readFileSync(path.join(ROOT, 'src/routes/internal.js'), 'utf8'); + const i = src.indexOf("runSnapshot(req.params.sport"); + const block = src.slice(i - 500, i + 300); + expect(block).toMatch(/trigger: acqT\.TRIGGER\.CONTROLLED_FORCED/); + expect(block).toMatch(/processStartedAt: PROCESS_STARTED_AT/); + }); + + test('the /snapshot/all route labels its runs CONTROLLED_FORCED too', () => { + const src = fs.readFileSync(path.join(ROOT, 'src/routes/internal.js'), 'utf8'); + const i = src.indexOf('runAllSnapshots({'); + expect(i).toBeGreaterThan(-1); + const block = src.slice(i - 200, i + 220); + expect(block).toMatch(/trigger: acqT\.TRIGGER\.CONTROLLED_FORCED/); + }); + + test('NEITHER internal route can label itself SCHEDULED', () => { + // Both routes appear in one file, so a single-occurrence edit can silently + // fix one and leave the other lying. + const src = fs.readFileSync(path.join(ROOT, 'src/routes/internal.js'), 'utf8'); + expect(src).not.toMatch(/trigger: acqT\.TRIGGER\.SCHEDULED/); + expect((src.match(/trigger: acqT\.TRIGGER\.CONTROLLED_FORCED/g) || [])).toHaveLength(2); + }); + + test('it does NOT pass a scheduled hour — there is no slot for a forced run', () => { + const src = fs.readFileSync(path.join(ROOT, 'src/routes/internal.js'), 'utf8'); + const i = src.indexOf("runSnapshot(req.params.sport"); + expect(src.slice(i - 500, i + 300)).not.toMatch(/scheduledHourUtc/); + }); + + test('the scheduler still labels its own runs SCHEDULED', () => { + const src = fs.readFileSync(path.join(ROOT, 'src/snapshotScheduler.js'), 'utf8'); + expect(src).toMatch(/trigger: acq\.TRIGGER\.SCHEDULED/); + expect(src).not.toMatch(/CONTROLLED_FORCED/); + }); +}); + +describe('RETENTION OF TRACES BY TRIGGER', () => { + const store = () => { + const seen = []; + return { seen, client: { lpush: async (k, v) => seen.push([k, JSON.parse(v).trigger]), ltrim: async () => {}, expire: async () => {} } }; + }; + + test('a CONTROLLED_FORCED trace IS retained, tagged as itself', async () => { + const s = store(); + const t = acq.finish(acq.begin({ sport: 'mlb', trigger: acq.TRIGGER.CONTROLLED_FORCED }), { final: acq.FINAL.NONZERO }); + const out = await acq.persist(t, { getRedisClient: () => s.client }); + expect(out.stored).toBe(true); + expect(s.seen[0]).toEqual(['ops:acquisition:mlb', 'CONTROLLED_FORCED']); + }); + + test('its pre-grading trace is retained too', async () => { + const s = store(); + const t = acq.finishPregrade(acq.beginPregrade({ attemptId: 'a', sport: 'mlb', trigger: acq.TRIGGER.CONTROLLED_FORCED }), {}); + expect((await acq.persistPregrade(t, { getRedisClient: () => s.client })).stored).toBe(true); + expect(s.seen[0][0]).toBe('ops:pregrade:mlb'); + }); + + test('INTRADAY is STILL refused — it would displace scheduled evidence', async () => { + const s = store(); + const t = acq.finish(acq.begin({ sport: 'mlb', trigger: acq.TRIGGER.INTRADAY }), { final: acq.FINAL.NONZERO }); + const out = await acq.persist(t, { getRedisClient: () => s.client }); + expect(out.stored).toBe(false); + expect(out.reason).toBe('not_retained_trigger'); + expect(s.seen).toHaveLength(0); + }); + + test('MANUAL_API is refused as well — only the two named triggers are kept', () => { + expect(acq.RETAINED_TRIGGERS).toEqual([acq.TRIGGER.SCHEDULED, acq.TRIGGER.CONTROLLED_FORCED]); + expect(acq.RETAINED_TRIGGERS).not.toContain(acq.TRIGGER.MANUAL); + }); +}); + +describe('THE PIPELINE PATH IS UNCHANGED', () => { + const src = fs.readFileSync(path.join(ROOT, 'src/routes/internal.js'), 'utf8'); + + test('the route still calls the SAME production runSnapshot', () => { + expect(src).toMatch(/const snapshot = require\('\.\.\/services\/snapshotService'\)/); + expect(src).toMatch(/snapshot\.runSnapshot\(req\.params\.sport, \{/); + }); + + test('it injects NO dependency override — production defaults throughout', () => { + const i = src.indexOf('runSnapshot(req.params.sport'); + const call = src.slice(i, src.indexOf('});', i) + 3); + for (const dep of ['getOdds', 'gradeAndCacheSlate', 'retention', 'ledger', 'cacheSet', + 'cacheGet', 'gameBinder', 'eventIdentity', 'mlbAdapter', 'notify']) { + expect(call).not.toContain(dep); + } + // Only the bisect limit and the two diagnostic fields. + expect(call).toMatch(/trigger:/); + expect(call).toMatch(/processStartedAt:/); + }); + + test('lineage is untouched by the trigger label', () => { + const rs = fs.readFileSync(path.join(ROOT, 'src/services/retentionService.js'), 'utf8'); + expect(rs).not.toMatch(/CONTROLLED_FORCED/); + }); +}); diff --git a/tests/unit/pregradeTrace.test.js b/tests/unit/pregradeTrace.test.js index f305b70..3b57fc5 100644 --- a/tests/unit/pregradeTrace.test.js +++ b/tests/unit/pregradeTrace.test.js @@ -273,7 +273,7 @@ describe('IDENTITY CORRELATION AND STORAGE', () => { 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_scheduled'); + expect(out.reason).toBe('not_retained_trigger'); }); test('two schedulers on the same slot are retained separately', async () => {