// LINEAGE CANARY LEASE — fail-closed, bounded, evaluated at the write gate. // // The defect this replaces: `LINEAGE_CANARY_SPORTS=mlb` was parsed once at // module load and frozen, so an enabled canary stayed writable for the entire // process lifetime. On 2026-08-29 it was left set and three scheduled snapshots // wrote lineage overnight unattended. The history was correct by luck; lineage // is append-only, so a defective canary would have written irreversible wrong // evidence just as quietly. const path = require('path'); const fs = require('fs'); const MOD = '../../src/services/lineageCanaryConfig'; const ROOT = path.join(__dirname, '../..'); /** Load the module under a given env value. The parse is at module load, so the * reload is how a "restart" is simulated. */ function load(val) { const prev = process.env.LINEAGE_CANARY_SPORTS; if (val === undefined) delete process.env.LINEAGE_CANARY_SPORTS; else process.env.LINEAGE_CANARY_SPORTS = val; jest.resetModules(); // eslint-disable-next-line global-require const m = require(MOD); if (prev === undefined) delete process.env.LINEAGE_CANARY_SPORTS; else process.env.LINEAGE_CANARY_SPORTS = prev; return m; } const at = (iso) => new Date(iso); const T0 = '2026-08-30T12:00:00Z'; const lease = (iso) => `mlb@${iso}`; describe('the bound is a function of the scheduler, and is proven not asserted', () => { test('MAX_LEASE cannot enclose three scheduled MLB ticks', () => { const cfg = load(undefined); // The scheduler's hours, read from the source of truth rather than retyped. const sched = fs.readFileSync(path.join(ROOT, 'src/config/sportCadence.js'), 'utf8'); const m = sched.match(/mlb:\s*\{\s*hours:\s*\[([0-9,\s]+)\]/); expect(m).toBeTruthy(); const HOURS = m[1].split(',').map((n) => parseInt(n.trim(), 10)); expect(HOURS.sort((a, b) => a - b)).toEqual([1, 3, 14, 19, 22]); // EXHAUSTIVE over a minute grid spanning UTC date boundaries. const ticks = []; for (let d = 0; d < 4; d += 1) for (const h of HOURS) ticks.push(d * 1440 + h * 60); ticks.sort((a, b) => a - b); const maxTicks = (durMin) => { let worst = 0; for (let start = 0; start <= 3 * 1440; start += 1) { const n = ticks.filter((t) => t >= start && t <= start + durMin).length; if (n > worst) worst = n; } return worst; }; const leaseMin = cfg.MAX_LEASE_MS / 60000; expect(leaseMin).toBe(240); expect(maxTicks(leaseMin)).toBeLessThanOrEqual(2); // The minimum three-tick span, stated so a scheduler change breaks this test // rather than silently invalidating the bound. let minSpan = Infinity; for (let i = 0; i + 2 < ticks.length; i += 1) minSpan = Math.min(minSpan, ticks[i + 2] - ticks[i]); expect(minSpan).toBe(300); // 22:00 -> 01:00 -> 03:00 expect(leaseMin).toBeLessThan(minSpan); // And the falsified claim: six hours DOES enclose three. expect(maxTicks(6 * 60)).toBe(3); }); }); describe('parse matrix — every invalid shape fails closed', () => { const cases = [ ['variable missing', undefined, 'OFF', false], ['empty string', '', 'OFF', false], ['whitespace only', ' ', 'OFF', false], ['LEGACY plain mlb', 'mlb', 'INVALID', false], ['legacy comma list', 'mlb,wnba', 'INVALID', false], ['two leases', 'mlb@2026-08-30T13:00:00Z,nba@2026-08-30T13:00:00Z', 'INVALID', false], ['duplicate @', 'mlb@2026-08-30T13:00:00Z@x', 'INVALID', false], ['missing sport', '@2026-08-30T13:00:00Z', 'INVALID', false], ['sport not leasable', 'nba@2026-08-30T13:00:00Z', 'INVALID', false], ['expiry without timezone', 'mlb@2026-08-30T13:00:00', 'INVALID', false], ['expiry with an offset', 'mlb@2026-08-30T13:00:00+00:00', 'INVALID', false], ['malformed expiry', 'mlb@not-a-date', 'INVALID', false], ['expiry is a duration', 'mlb@4h', 'INVALID', false], ['trailing junk', 'mlb@2026-08-30T13:00:00Z junk', 'INVALID', false], ['beyond MAX_LEASE', 'mlb@2026-08-30T17:00:00.001Z', 'INVALID', false], ['already expired', 'mlb@2026-08-30T11:59:59Z', 'EXPIRED', false], ['expiry exactly now', 'mlb@2026-08-30T12:00:00Z', 'EXPIRED', false], ['valid future lease', 'mlb@2026-08-30T13:00:00Z', 'ACTIVE', true], ['valid at the exact bound', 'mlb@2026-08-30T16:00:00Z', 'ACTIVE', true], ]; for (const [name, val, expectState, expectEnabled] of cases) { test(`${name} -> ${expectState}`, () => { const cfg = load(val); const s = cfg.state(at(T0)); expect(s.lease_state).toBe(expectState); expect(s.effective_enabled).toBe(expectEnabled); expect(cfg.isEnabled('mlb', at(T0))).toBe(expectEnabled); if (!expectEnabled) expect(s.effective_active_sports).toEqual([]); }); } test('an unreadable clock fails closed rather than defaulting to active', () => { const cfg = load(lease('2026-08-30T13:00:00Z')); expect(cfg.isEnabled('mlb', new Date('nonsense'))).toBe(false); expect(cfg.state(new Date('nonsense')).lease_state).toBe('INVALID'); expect(cfg.state(new Date('nonsense')).invalid_reason).toBe('INVALID_CLOCK'); }); test('a valid lease enables ONLY its own sport', () => { const cfg = load(lease('2026-08-30T13:00:00Z')); expect(cfg.isEnabled('mlb', at(T0))).toBe(true); for (const sp of ['wnba', 'nba', 'soccer', 'nfl']) { expect(cfg.isEnabled(sp, at(T0))).toBe(false); } }); }); describe('expiry is dynamic — no restart required', () => { test('one process, one parse: ACTIVE then EXPIRED as the clock crosses', () => { const cfg = load(lease('2026-08-30T13:00:00Z')); // Same module instance throughout — this is the whole point. expect(cfg.isEnabled('mlb', at('2026-08-30T12:59:59Z'))).toBe(true); expect(cfg.state(at('2026-08-30T12:59:59Z')).lease_state).toBe('ACTIVE'); expect(cfg.isEnabled('mlb', at('2026-08-30T13:00:00Z'))).toBe(false); expect(cfg.state(at('2026-08-30T13:00:00Z')).lease_state).toBe('EXPIRED'); expect(cfg.isEnabled('mlb', at('2026-08-30T14:00:00Z'))).toBe(false); }); test('remaining_ms counts down and floors at expiry', () => { const cfg = load(lease('2026-08-30T13:00:00Z')); expect(cfg.state(at('2026-08-30T12:00:00Z')).remaining_ms).toBe(3600000); expect(cfg.state(at('2026-08-30T12:30:00Z')).remaining_ms).toBe(1800000); expect(cfg.state(at('2026-08-30T13:30:00Z')).remaining_ms).toBe(0); }); }); describe('restart semantics — absolute expiry controls', () => { test('a restart BEFORE expiry does not reset the lease lifetime', () => { const val = lease('2026-08-30T13:00:00Z'); const first = load(val); expect(first.state(at('2026-08-30T12:10:00Z')).remaining_ms).toBe(3000000); // "restart" — reload the module, same env value. const second = load(val); expect(second.state(at('2026-08-30T12:50:00Z')).remaining_ms).toBe(600000); expect(second.state(at('2026-08-30T12:50:00Z')).configured_expires_at) .toBe(first.state(at('2026-08-30T12:10:00Z')).configured_expires_at); }); test('a restart AFTER expiry does NOT reactivate it', () => { const cfg = load(lease('2026-08-30T11:00:00Z')); expect(cfg.isEnabled('mlb', at(T0))).toBe(false); expect(cfg.state(at(T0)).lease_state).toBe('EXPIRED'); }); }); describe('configured-but-expired stays auditable', () => { test('EXPIRED is distinguishable from never-configured', () => { const off = load(undefined).state(at(T0)); const exp = load(lease('2026-08-30T11:00:00Z')).state(at(T0)); expect(off.configured).toBe(false); expect(off.lease_state).toBe('OFF'); expect(exp.configured).toBe(true); expect(exp.config_valid).toBe(true); expect(exp.lease_state).toBe('EXPIRED'); expect(exp.configured_sport).toBe('mlb'); expect(exp.configured_expires_at).toBe('2026-08-30T11:00:00.000Z'); expect(exp.effective_enabled).toBe(false); expect(exp.effective_active_sports).toEqual([]); }); test('the status never returns the raw environment string', () => { const raw = lease('2026-08-30T13:00:00Z'); const s = JSON.stringify(load(raw).state(at(T0))); expect(s).not.toContain('LINEAGE_CANARY_SPORTS'); }); }); describe('ONE evaluator — status and writer cannot disagree', () => { test('the snapshot write gate delegates and threads the clock', () => { const src = fs.readFileSync(path.join(ROOT, 'src/services/snapshotService.js'), 'utf8'); expect(src).toMatch(/function lineageCanaryEnabled\(sport, now\)/); expect(src).toMatch(/lineageCanaryConfig\.isEnabled\(sport, now\)/); // The gate must not cache the ANSWER at module scope. A frozen boolean is // the original defect, so this pins the shape rather than one spelling of // it: `isEnabled` may only be called from inside the function body. const decl = src.slice(src.indexOf('function lineageCanaryEnabled')); const body = decl.slice(0, decl.indexOf('\n}') + 2); expect(body).toMatch(/lineageCanaryConfig\.isEnabled\(sport, now\)/); // No module-scope evaluation of the gate anywhere outside that function. const outside = src.replace(body, ''); expect(outside).not.toMatch(/lineageCanaryConfig\.isEnabled\(/); expect(outside).not.toMatch(/=\s*lineageCanaryEnabled\(/); expect(src).toMatch(/commitPublication && persistedRows && lineageCanaryEnabled\(sp\)/); }); test('for the same config and instant, writer and status agree', () => { for (const [val, when] of [ [lease('2026-08-30T13:00:00Z'), '2026-08-30T12:30:00Z'], [lease('2026-08-30T13:00:00Z'), '2026-08-30T13:30:00Z'], ['mlb', '2026-08-30T12:00:00Z'], [undefined, '2026-08-30T12:00:00Z'], ]) { const cfg = load(val); jest.resetModules(); if (val === undefined) delete process.env.LINEAGE_CANARY_SPORTS; else process.env.LINEAGE_CANARY_SPORTS = val; // eslint-disable-next-line global-require const snap = require('../../src/services/snapshotService'); // eslint-disable-next-line global-require const live = require(MOD); delete process.env.LINEAGE_CANARY_SPORTS; const s = live.state(at(when)); expect(snap.lineageCanaryEnabled('mlb', at(when))).toBe(s.effective_enabled); expect(live.isEnabled('mlb', at(when))).toBe(s.effective_enabled); expect(cfg.isEnabled('mlb', at(when))).toBe(s.effective_enabled); } }); }); describe('preservation — only the activation gate changed', () => { const src = (f) => fs.readFileSync(path.join(ROOT, f), 'utf8'); test('lineage algorithms are untouched', () => { const rl = src('src/services/read/readLineage.js'); expect(rl).toContain("const LINEAGE_VERSION = 'lin@1'"); expect(rl).toContain("const CLAIM_SCHEMA_VERSION = 'claim@1'"); expect(rl).toContain("const DIGEST_ALGORITHM_VERSION = 'sha256-json-sorted@1'"); const ret = src('src/services/retentionService.js'); expect(ret).toContain('familyScopesFrom'); expect(ret).toContain('isValidLineageAction'); // Pin the IDENTITY ITSELF, not merely the constant's name — a changed tuple // with an unchanged name is exactly the drift a name check cannot see. expect(ret).toContain( "const RETENTION_CONFLICT = 'snapshot_id,game_id,canonical_event_id,player_key,stat,line,side';"); expect(ret).toContain( "const VALID_LINEAGE_ACTION_FIELDS = Object.freeze(["); }); test('cache-date, participant and intraday repairs are untouched', () => { expect(src('src/services/oddsService.js')).toContain("ET_DATE_SPORTS = Object.freeze(['mlb'])"); expect(src('src/services/event/eventIdentity.js')).toMatch(/ids\.size !== 1/); expect(src('src/services/gameBinder.js')).toContain('if (!p.game_date) p.game_date = etFast;'); expect(src('src/services/intradayRefreshService.js')).toContain('BELIEF_FIELDS'); }); });