A canary is temporary: bound activation with a fail-closed lease

`LINEAGE_CANARY_SPORTS=mlb` was parsed once at module load and frozen, so an
enabled canary stayed writable for the whole process lifetime. On 2026-08-29 it
was left set and the 22:00Z, 01:00Z and 03:00Z scheduled snapshots each wrote
lineage overnight with nobody watching. That history happened to be correct.
Lineage is append-only evidence, so a defective canary would have written
irreversible WRONG evidence exactly as quietly. Correct history was luck, not a
safety property.

NEW CONTRACT:  LINEAGE_CANARY_SPORTS=mlb@2026-08-30T18:00:00Z

Absolute UTC instant only -- no duration, no local timezone. A relative "4h"
would silently restart on every redeploy, which is the defect being removed.

LEGACY `mlb` NO LONGER ACTIVATES ANYTHING. It is INVALID_MISSING_EXPIRY. Leaving
it working would have left the defect in place behind a nicer-looking
alternative, so this is the load-bearing half of the repair.

EXPIRY IS EVALUATED AT THE WRITE GATE, not at startup. `isEnabled(sport, now)`
re-reads the clock on every call and `lineageCanaryEnabled` threads it through,
so a lease turns itself off with no operator, no restart, no Redis and no
network. A design where an expired canary keeps writing until someone restarts
is the same failure in a different hat.

MAX_LEASE is FOUR HOURS, and the bound is proven rather than chosen. MLB ticks
are [14,19,22,1,3] UTC; exhaustively over a minute grid across UTC date
boundaries, the shortest span enclosing THREE consecutive ticks is
22:00 -> 01:00 -> 03:00 = five hours. Four hours encloses at most two, with an
hour of margin. (An earlier note claimed six hours admitted two. It admits
three; that claim was false and the test now pins the arithmetic.) The bound is
a function of the scheduler, so a test reads the hours from sportCadence and
fails if a cadence change invalidates the proof.

Everything fails closed: absent, empty, bare sport, comma list, two leases,
duplicate @, non-leasable sport, missing timezone, numeric offset, malformed,
over-long, already-expired, expiry-equals-now, and an unreadable clock.
Configured-but-EXPIRED stays distinguishable from never-configured so automatic
containment is auditable.

No Redis, no Supabase, no counter, no network in the lease path -- an
unreachable dependency must never decide whether lineage may write.

Lineage algorithms, cache-date, participant and retention identity untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
This commit is contained in:
Kev
2026-08-30 14:58:11 -04:00
parent 836b8c73d5
commit cc5bdf5797
5 changed files with 529 additions and 93 deletions
+196 -55
View File
@@ -1,79 +1,220 @@
'use strict';
"use strict";
/**
* LINEAGE CANARY CONFIG — the ONE resolver.
* LINEAGE CANARY LEASE — the ONE resolver, now BOUNDED IN TIME.
*
* -- WHY THIS MODULE EXISTS ----------------------------------------------
* The rollout stalled at RUNTIME_UNVERIFIED because nothing could answer
* "is MLB lineage effectively enabled?" without waiting for a scheduled
* snapshot to write a row. Exposing the answer requires a status route to read
* the config — and a status route that parses the environment ITSELF would be a
* second version of the truth, free to drift from the gate it claims to report.
* -- WHY THIS CHANGED -----------------------------------------------------
* The previous contract parsed `LINEAGE_CANARY_SPORTS=mlb` once at module load
* and froze the answer. `isEnabled()` then returned the same boolean for the
* lifetime of the process. On 2026-08-29 the flag was left set; the 22:00Z,
* 01:00Z and 03:00Z scheduled snapshots each wrote lineage overnight with no
* operator present. The history they wrote happened to be correct. That is luck,
* not a safety property: lineage is append-only evidence, so a defective canary
* would have written irreversible wrong evidence just as silently.
*
* So the parse lives here, once. `snapshotService` consumes it for the actual
* lineage write gate, and the internal status probe consumes the SAME frozen
* state. Observability reports the reality the system acts on, or it is not
* observability.
* A canary is temporary BY DEFINITION. This encodes that into the activation
* contract instead of relying on somebody remembering.
*
* -- EFFECTIVE-TIME SEMANTICS --------------------------------------------
* The value is resolved ONCE, at module load, exactly as the previous inline
* constant was. It is therefore FIXED FOR THE LIFETIME OF THE PROCESS:
* -- THE SYNTAX -----------------------------------------------------------
* LINEAGE_CANARY_SPORTS=mlb@2026-08-30T18:00:00Z
* ^^^ ^^^^^^^^^^^^^^^^^^^^
* sport absolute UTC expiry
*
* read from process.env yes
* parsed once at module startup yes
* re-read per lineage decision no
* can change without a restart no
* Absolute instant only. No duration ("4h"), no local timezone, no implicit
* offset -- a relative duration would silently restart on every redeploy, which
* is the exact defect being removed.
*
* That is what makes the process start time a defensible lower bound for
* "this runtime has had this effective lineage state since at least then".
* Changing the variable in Coolify restarts the container, which yields a new
* process and therefore a new boundary.
* -- LEGACY `mlb` NO LONGER ACTIVATES ANYTHING ---------------------------
* The bare sport form is now INVALID_MISSING_EXPIRY, not a valid indefinite
* activation. This is load-bearing: leaving it working would leave the defect
* in place behind a nicer-looking alternative.
*
* -- EXPIRY IS EVALUATED AT THE WRITE GATE, NOT AT STARTUP ---------------
* Parsing the raw string once is fine; it cannot change without a restart.
* EFFECTIVE ENABLEMENT is not frozen. `isEnabled(sport, now)` re-reads the clock
* on every call, so expiry takes effect with no operator, no restart, no Redis
* and no network. A design where an expired canary keeps writing until someone
* restarts it is the same failure wearing a different hat.
*
* -- NO EXTERNAL DEPENDENCY ----------------------------------------------
* Lease truth is local parsed configuration plus the wall clock. An unreachable
* Redis or Supabase must never be what decides whether lineage may write.
*/
/** Where the effective value came from. Never the value itself. */
const CONFIGURATION_SOURCE = Object.freeze({
ENVIRONMENT: 'ENVIRONMENT',
DEFAULT: 'DEFAULT',
ENVIRONMENT: "ENVIRONMENT",
DEFAULT: "DEFAULT",
});
/** Lease states, in the language the status probe reports. */
const LEASE_STATE = Object.freeze({
OFF: "OFF", // nothing configured
ACTIVE: "ACTIVE", // configured, valid, and now < expires_at
EXPIRED: "EXPIRED", // configured and valid, but the clock passed expiry
INVALID: "INVALID", // configured and unusable — always fails closed
});
/**
* THE BOUND, and why it is four hours.
*
* MLB scheduled snapshot ticks are [14,19,22,1,3] UTC. Proven exhaustively over
* a minute grid across UTC date boundaries: the SHORTEST span enclosing three
* consecutive ticks is 22:00 -> 01:00 -> 03:00 = FIVE HOURS. A lease of four
* hours therefore encloses at most TWO scheduled ticks, with an hour of margin.
*
* (An earlier design note claimed six hours admitted two ticks. That was wrong:
* six hours spans 22->01->03 and admits three.)
*
* THIS BOUND IS A FUNCTION OF THE SCHEDULER. Changing SNAPSHOT_HOURS_UTC
* invalidates the proof and requires revalidating MAX_LEASE_MS. A test pins
* both the hours and the bound together so the two cannot drift apart.
*/
const MAX_LEASE_MS = 4 * 60 * 60 * 1000;
/** The sports this rollout may lease. Widening is a deliberate, separate act. */
const LEASABLE_SPORTS = Object.freeze(["mlb"]);
const RAW = process.env.LINEAGE_CANARY_SPORTS;
/**
* Normalised, deterministic, frozen. Sorted so two processes with the same
* effective configuration report byte-identical state regardless of the order
* it was written in.
*/
const SPORTS = Object.freeze(
String(RAW || '')
.split(',')
.map((x) => x.trim().toLowerCase())
.filter(Boolean)
.filter((x, i, a) => a.indexOf(x) === i)
.sort(),
);
/**
* DEFAULT means the variable was never set — the dark default. ENVIRONMENT
* means something set it, INCLUDING setting it to empty, because an explicit
* empty is an operator decision and reads differently from an absent one.
*/
const SOURCE = RAW === undefined ? CONFIGURATION_SOURCE.DEFAULT : CONFIGURATION_SOURCE.ENVIRONMENT;
/** THE gate. Lineage writes for a sport only if this says so. */
function isEnabled(sport) {
return SPORTS.includes(String(sport || '').toLowerCase());
/**
* A strict absolute-UTC ISO instant. Deliberately narrow: `Date.parse` accepts
* plenty of shapes whose meaning depends on the reader's timezone, and a lease
* that means different things on different containers is not a lease.
*/
const ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
/**
* Parse the raw value ONCE. Returns a frozen description — never a decision.
* The decision needs a clock and is made in `evaluate`.
*/
function parseLease(raw) {
const bad = (reason) => Object.freeze({
configured: true, valid: false, invalid_reason: reason, sport: null, expires_at: null,
});
if (raw === undefined || raw === null) {
return Object.freeze({ configured: false, valid: false, invalid_reason: null, sport: null, expires_at: null });
}
const s = String(raw).trim();
if (s === "") {
// An explicit empty is an operator decision to be off, not a malformed one.
return Object.freeze({ configured: false, valid: false, invalid_reason: null, sport: null, expires_at: null });
}
// ONE token only. Two sports leased at once is how a canary quietly widens.
if (s.includes(",")) return bad("INVALID_MULTIPLE_TOKENS");
const at = s.indexOf("@");
if (at === -1) return bad("INVALID_MISSING_EXPIRY"); // the legacy bare `mlb`
if (s.indexOf("@", at + 1) !== -1) return bad("INVALID_SYNTAX");
const sport = s.slice(0, at).trim().toLowerCase();
const stamp = s.slice(at + 1).trim();
if (!sport) return bad("INVALID_MISSING_SPORT");
if (!LEASABLE_SPORTS.includes(sport)) return bad("INVALID_SPORT_NOT_LEASABLE");
if (!ISO_UTC.test(stamp)) return bad("INVALID_EXPIRY_FORMAT");
const ms = Date.parse(stamp);
if (!Number.isFinite(ms)) return bad("INVALID_EXPIRY_VALUE");
return Object.freeze({
configured: true, valid: true, invalid_reason: null,
sport, expires_at: new Date(ms).toISOString(), expires_ms: ms,
});
}
const LEASE = parseLease(RAW);
/** The clock, as a finite epoch-ms, or null. A clock we cannot read is not a
* reason to allow writing. */
function nowMs(now) {
const d = now === undefined ? new Date() : now;
const ms = d instanceof Date ? d.getTime() : Date.parse(d);
return Number.isFinite(ms) ? ms : null;
}
/**
* The reportable state. Contains no raw environment value by construction —
* `RAW` is never returned, only the normalised set derived from it.
* THE ONE EVALUATOR. The write gate, the status probe and the tests all come
* here, so a status that says EXPIRED can never sit beside a writer that still
* writes.
*/
function state() {
return {
enabled: SPORTS.length > 0,
sports: [...SPORTS],
function evaluate(now) {
const t = nowMs(now);
const base = {
configured: LEASE.configured,
configuration_source: SOURCE,
config_valid: LEASE.valid,
invalid_reason: LEASE.invalid_reason,
configured_sport: LEASE.sport,
configured_expires_at: LEASE.expires_at,
lease_state: LEASE_STATE.OFF,
effective_enabled: false,
effective_active_sports: [],
remaining_ms: null,
};
if (!LEASE.configured) return Object.freeze(base);
if (!LEASE.valid) return Object.freeze({ ...base, lease_state: LEASE_STATE.INVALID });
if (t === null) {
// Fail closed, and say WHY rather than reporting a bare OFF.
return Object.freeze({ ...base, lease_state: LEASE_STATE.INVALID, invalid_reason: "INVALID_CLOCK" });
}
const remaining = LEASE.expires_ms - t;
// A lease longer than the bound is refused outright — a week-long "canary" is
// the defect this module exists to remove.
if (remaining > MAX_LEASE_MS) {
return Object.freeze({ ...base, lease_state: LEASE_STATE.INVALID, invalid_reason: "INVALID_LEASE_TOO_LONG" });
}
// `<= 0` covers both expiry and an expiry set at or before now. EXPIRED is
// kept distinct from OFF so automatic containment stays auditable.
if (remaining <= 0) {
return Object.freeze({ ...base, lease_state: LEASE_STATE.EXPIRED, remaining_ms: 0 });
}
return Object.freeze({
...base,
lease_state: LEASE_STATE.ACTIVE,
effective_enabled: true,
effective_active_sports: [LEASE.sport],
remaining_ms: remaining,
});
}
/** THE gate. Lineage writes for a sport only if this says so, RIGHT NOW. */
function isEnabled(sport, now) {
const e = evaluate(now);
if (!e.effective_enabled) return false;
return e.effective_active_sports.includes(String(sport || "").toLowerCase());
}
/** The reportable state. Contains no raw environment value by construction. */
function state(now) {
const e = evaluate(now);
return {
// Back-compatible keys the probe and its tests already consume.
enabled: e.effective_enabled,
sports: [...e.effective_active_sports],
configuration_source: e.configuration_source,
// The lease surface: configured vs EFFECTIVE, so an expired lease is
// visibly different from one that was never set.
configured: e.configured,
config_valid: e.config_valid,
invalid_reason: e.invalid_reason,
configured_sport: e.configured_sport,
configured_expires_at: e.configured_expires_at,
lease_state: e.lease_state,
effective_enabled: e.effective_enabled,
effective_active_sports: [...e.effective_active_sports],
remaining_ms: e.remaining_ms,
max_lease_ms: MAX_LEASE_MS,
};
}
module.exports = { SPORTS, isEnabled, state, CONFIGURATION_SOURCE };
/**
* The CONFIGURED sport list. Deliberately NOT the gate: it says what was asked
* for, never what is permitted now.
*/
const SPORTS = Object.freeze(LEASE.valid && LEASE.sport ? [LEASE.sport] : []);
module.exports = {
SPORTS, isEnabled, state, evaluate, parseLease,
CONFIGURATION_SOURCE, LEASE_STATE, MAX_LEASE_MS, LEASABLE_SPORTS,
};
+5 -2
View File
@@ -319,8 +319,11 @@ const lineageCanaryConfig = require('./lineageCanaryConfig');
const LINEAGE_CANARY_SPORTS = lineageCanaryConfig.SPORTS;
function lineageCanaryEnabled(sport) {
return lineageCanaryConfig.isEnabled(sport);
// `now` is threaded so the LEASE is evaluated at WRITE TIME, not at startup.
// A cached boolean here would reintroduce the exact defect the lease removes:
// an expired canary that keeps writing until somebody restarts the process.
function lineageCanaryEnabled(sport, now) {
return lineageCanaryConfig.isEnabled(sport, now);
}
/**
+20 -9
View File
@@ -443,25 +443,36 @@ describe('CANARY SCOPE — bounded and reversible', () => {
}
});
test('MLB is enabled only by explicit configuration', () => {
const m = load('mlb');
expect(m.lineageCanaryEnabled('mlb')).toBe(true);
test('MLB is enabled only by an explicit BOUNDED LEASE', () => {
// Plain `mlb` is no longer an activation path — it means "forever", which
// is precisely the containment defect. Only sport@absolute-UTC-expiry works.
const now = new Date('2026-08-30T12:00:00Z');
const legacy = load('mlb');
expect(legacy.lineageCanaryEnabled('mlb', now)).toBe(false);
const m = load('mlb@2026-08-30T13:00:00Z');
expect(m.lineageCanaryEnabled('mlb', now)).toBe(true);
for (const sp of ['wnba', 'nba', 'soccer']) {
expect(m.lineageCanaryEnabled(sp)).toBe(false);
expect(m.lineageCanaryEnabled(sp, now)).toBe(false);
}
// And it turns itself off when the clock crosses, with no restart.
expect(m.lineageCanaryEnabled('mlb', new Date('2026-08-30T13:00:01Z'))).toBe(false);
});
test('an empty value is a kill switch needing no migration revert', () => {
const m = load('');
expect(m.LINEAGE_CANARY_SPORTS).toEqual([]);
expect(m.lineageCanaryEnabled('mlb')).toBe(false);
expect(m.lineageCanaryEnabled('mlb', new Date('2026-08-30T12:00:00Z'))).toBe(false);
});
test('a sport without a verified event resolver cannot be canaried into safety', () => {
// Widening the flag does NOT create identity: eventIdentity still refuses,
// so a widened sport records UNSUPPORTED_SPORT and its lineage stays honest.
const m = load('mlb,wnba');
expect(m.lineageCanaryEnabled('wnba')).toBe(true);
// Two defences now. The lease refuses to widen at all (one sport, and only
// a leasable one), AND eventIdentity still refuses to invent identity, so a
// widened sport would record UNSUPPORTED_SPORT and stay honest regardless.
const now = new Date('2026-08-30T12:00:00Z');
const m = load('mlb@2026-08-30T13:00:00Z,wnba@2026-08-30T13:00:00Z');
expect(m.lineageCanaryEnabled('wnba', now)).toBe(false);
expect(m.lineageCanaryEnabled('mlb', now)).toBe(false); // the WHOLE config fails closed
const r = E.resolveEvent('wnba', dhProp('2026-08-17T17:40:00Z'), DH_GAMES);
expect(r.canonical_event_id).toBeNull();
expect(r.event_identity_method).toBe(E.IDENTITY_METHOD.UNSUPPORTED_SPORT);
+244
View File
@@ -0,0 +1,244 @@
// 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');
});
});
+64 -27
View File
@@ -113,54 +113,86 @@ describe('LINEAGE CONFIG — one parser', () => {
const snap = require('../../src/services/snapshotService');
// eslint-disable-next-line global-require
const cfg = require('../../src/services/lineageCanaryConfig');
const now = new Date('2026-08-30T12:00:00Z');
for (const sp of ['mlb', 'wnba', 'nba', 'soccer']) {
expect(snap.lineageCanaryEnabled(sp)).toBe(cfg.isEnabled(sp));
expect(snap.lineageCanaryEnabled(sp, now)).toBe(cfg.isEnabled(sp, now));
}
expect(snap.LINEAGE_CANARY_SPORTS).toBe(cfg.SPORTS);
});
test('unset reports disabled + [] + DEFAULT', () => {
test('unset reports OFF + [] + DEFAULT', () => {
const c = loadConfig(undefined);
expect(c.state()).toEqual({ enabled: false, sports: [], configuration_source: 'DEFAULT' });
const st = c.state(new Date('2026-08-30T12:00:00Z'));
expect(st.enabled).toBe(false);
expect(st.sports).toEqual([]);
expect(st.configuration_source).toBe('DEFAULT');
expect(st.lease_state).toBe('OFF');
expect(st.configured).toBe(false);
});
test('mlb reports enabled + ["mlb"] + ENVIRONMENT', () => {
test('LEGACY plain `mlb` no longer activates anything', () => {
// This is the safety repair, expressed as a test. The bare sport form used
// to mean "write forever until somebody remembers"; it is now refused.
const c = loadConfig('mlb');
expect(c.state()).toEqual({ enabled: true, sports: ['mlb'], configuration_source: 'ENVIRONMENT' });
const st = c.state(new Date('2026-08-30T12:00:00Z'));
expect(st.enabled).toBe(false);
expect(st.effective_enabled).toBe(false);
expect(st.sports).toEqual([]);
expect(st.lease_state).toBe('INVALID');
expect(st.invalid_reason).toBe('INVALID_MISSING_EXPIRY');
expect(c.isEnabled('mlb', new Date('2026-08-30T12:00:00Z'))).toBe(false);
});
test('a bounded lease reports ACTIVE + ["mlb"] + ENVIRONMENT', () => {
const c = loadConfig('mlb@2026-08-30T13:00:00Z');
const st = c.state(new Date('2026-08-30T12:00:00Z'));
expect(st.enabled).toBe(true);
expect(st.sports).toEqual(['mlb']);
expect(st.configuration_source).toBe('ENVIRONMENT');
expect(st.lease_state).toBe('ACTIVE');
expect(st.configured_expires_at).toBe('2026-08-30T13:00:00.000Z');
});
test('an EXPLICIT empty is ENVIRONMENT, not DEFAULT', () => {
// An operator deliberately blanking the value reads differently from never
// having set it, even though both are dark.
const c = loadConfig('');
expect(c.state()).toEqual({ enabled: false, sports: [], configuration_source: 'ENVIRONMENT' });
const st = c.state(new Date('2026-08-30T12:00:00Z'));
expect(st.enabled).toBe(false);
expect(st.sports).toEqual([]);
expect(st.configuration_source).toBe('ENVIRONMENT');
expect(st.lease_state).toBe('OFF');
});
test('multiple sports normalise deterministically — sorted, deduped, trimmed, lowercased', () => {
expect(loadConfig('MLB, mlb ,wnba').state().sports).toEqual(['mlb', 'wnba']);
expect(loadConfig('wnba,mlb').state().sports).toEqual(['mlb', 'wnba']);
expect(loadConfig(' MLB ').state().sports).toEqual(['mlb']);
expect(loadConfig(',,mlb,,').state().sports).toEqual(['mlb']);
test('a comma list can no longer widen the canary', () => {
for (const v of ['MLB, mlb ,wnba', 'wnba,mlb', 'mlb@2026-08-30T13:00:00Z,nba@2026-08-30T13:00:00Z']) {
const st = loadConfig(v).state(new Date('2026-08-30T12:00:00Z'));
expect(st.effective_enabled).toBe(false);
expect(st.sports).toEqual([]);
expect(st.lease_state).toBe('INVALID');
}
});
test('an unsupported value behaves exactly as the gate behaves', () => {
const c = loadConfig('cricket');
expect(c.state().sports).toEqual(['cricket']);
expect(c.isEnabled('cricket')).toBe(true); // the flag is scope, not validation
expect(c.isEnabled('mlb')).toBe(false);
test('a sport outside the leasable set cannot be leased', () => {
const c = loadConfig('cricket@2026-08-30T13:00:00Z');
const st = c.state(new Date('2026-08-30T12:00:00Z'));
expect(st.lease_state).toBe('INVALID');
expect(st.invalid_reason).toBe('INVALID_SPORT_NOT_LEASABLE');
expect(c.isEnabled('cricket', new Date('2026-08-30T12:00:00Z'))).toBe(false);
expect(c.isEnabled('mlb', new Date('2026-08-30T12:00:00Z'))).toBe(false);
});
test('the source-code default cannot hide an environment override', () => {
// The whole point: if Coolify sets mlb, the probe must say mlb.
expect(loadConfig('mlb').state().enabled).toBe(true);
expect(loadConfig(undefined).state().enabled).toBe(false);
const now = new Date('2026-08-30T12:00:00Z');
expect(loadConfig('mlb@2026-08-30T13:00:00Z').state(now).enabled).toBe(true);
expect(loadConfig(undefined).state(now).enabled).toBe(false);
});
});
describe('SECURITY — no raw config, no weakened access', () => {
test('the raw environment value is never returned', () => {
const c = loadConfig('MLB, wnba ');
const payload = JSON.stringify(c.state());
const payload = JSON.stringify(c.state(new Date('2026-08-30T12:00:00Z')));
expect(payload).not.toContain('MLB, wnba');
expect(payload).not.toContain(' ');
});
@@ -195,18 +227,23 @@ describe('READ ONLY — the probe observes and nothing else', () => {
test('it cannot enable lineage — it only reads the frozen state', () => {
const c = loadConfig(undefined);
const before = JSON.stringify(c.state());
c.state(); c.state();
expect(JSON.stringify(c.state())).toBe(before);
const fixed = new Date('2026-08-30T12:00:00Z');
const before = JSON.stringify(c.state(fixed));
c.state(fixed); c.state(fixed);
expect(JSON.stringify(c.state(fixed))).toBe(before);
// No setter exists.
expect(Object.keys(c).filter((k) => /^(set|enable|disable|update)/i.test(k))).toHaveLength(0);
});
test('the reported state is a copy — a caller cannot mutate the gate', () => {
const c = loadConfig('mlb');
const s1 = c.state();
const c = loadConfig('mlb@2026-08-30T13:00:00Z');
const now = new Date('2026-08-30T12:00:00Z');
const s1 = c.state(now);
s1.sports.push('wnba');
expect(c.state().sports).toEqual(['mlb']);
expect(c.isEnabled('wnba')).toBe(false);
s1.effective_active_sports.push('nba');
expect(c.state(now).sports).toEqual(['mlb']);
expect(c.state(now).effective_active_sports).toEqual(['mlb']);
expect(c.isEnabled('wnba', now)).toBe(false);
expect(c.isEnabled('nba', now)).toBe(false);
});
});