1728ce4fac
Found by cross-checking the observer against an independent SQL derivation of the same natural cohort instead of accepting its own answer. The 2026-08-31 03:00Z run held 1,970 rows across TWO game_dates — 1,572 on 08-31 and 398 on 08-30, the late slot spanning midnight ET. The observer selected the head row's date and filtered to it, so it measured 646 of the 801 eligible keys and reported HEALTHY. Both slices happened to be fully covered, so the verdict was right by luck; a date slice with zero lineage would have been invisible behind it. The audit now covers the whole snapshot_id and reports every game_date it spans. A test builds the real two-date shape with one slice uncovered and requires PARTIAL_COVERAGE. Suite 397/5,499/0 · 15/15 teeth. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
544 lines
25 KiB
JavaScript
544 lines
25 KiB
JavaScript
/**
|
|
* LINEAGE PRODUCTIZATION — persistent shadow writer, independent coverage
|
|
* observer, additive ancestry surface.
|
|
*
|
|
* Three properties this suite exists to hold:
|
|
* the product never depends on lineage;
|
|
* the observer never trusts the writer;
|
|
* the ancestry surface never invents history it does not have.
|
|
*/
|
|
|
|
const svc = require('../../src/services/snapshotService');
|
|
const mode = require('../../src/services/lineageWriteMode');
|
|
const coverage = require('../../src/services/lineageCoverage');
|
|
const ancestry = require('../../src/services/read/readAncestry');
|
|
const retention = require('../../src/services/retentionService');
|
|
|
|
const ENV = mode.ENV_VAR;
|
|
const setPersistent = (v) => { if (v === null) delete process.env[ENV]; else process.env[ENV] = v; };
|
|
afterEach(() => setPersistent(null));
|
|
|
|
/* ─────────────────────────── WRITE MODE ─────────────────────────── */
|
|
describe('write mode — three modes, one evaluator', () => {
|
|
test('unset -> OFF, nothing enabled', () => {
|
|
setPersistent(null);
|
|
expect(mode.evaluate().mode).toBe(mode.WRITE_MODE.OFF);
|
|
expect(mode.isEnabled('mlb')).toBe(false);
|
|
});
|
|
|
|
test('PERSISTENT_SHADOW writes without any lease', () => {
|
|
setPersistent('mlb');
|
|
const e = mode.evaluate();
|
|
expect(e.mode).toBe(mode.WRITE_MODE.PERSISTENT_SHADOW);
|
|
expect(mode.isEnabled('mlb')).toBe(true);
|
|
// No lease is involved, and none is required.
|
|
expect(e.canary_sports).toEqual([]);
|
|
expect(mode.modeFor('mlb')).toBe(mode.WRITE_MODE.PERSISTENT_SHADOW);
|
|
});
|
|
|
|
test('a sport outside the allowlist fails closed, and takes the whole list with it', () => {
|
|
setPersistent('wnba');
|
|
expect(mode.evaluate().persistent_invalid_reason).toBe('INVALID_SPORT_NOT_PERSISTABLE');
|
|
expect(mode.isEnabled('wnba')).toBe(false);
|
|
setPersistent('mlb,wnba');
|
|
// Keeping the valid half would enable a subset nobody asked for on its own.
|
|
expect(mode.isEnabled('mlb')).toBe(false);
|
|
});
|
|
|
|
test('the allowlist is the canary\'s, not a second one', () => {
|
|
const canary = require('../../src/services/lineageCanaryConfig');
|
|
expect(mode.PERSISTABLE_SPORTS).toBe(canary.LEASABLE_SPORTS);
|
|
});
|
|
|
|
test('another sport cannot become enabled by accident', () => {
|
|
setPersistent('mlb');
|
|
for (const s of ['wnba', 'nba', 'soccer', '']) expect(mode.isEnabled(s)).toBe(false);
|
|
});
|
|
|
|
test('status and gate come from the SAME evaluator', () => {
|
|
setPersistent('mlb');
|
|
const st = mode.state();
|
|
expect(st.mode).toBe(mode.evaluate().mode);
|
|
expect(st.effective_sports).toEqual(mode.evaluate().sports);
|
|
expect(st.authority).toBe('NON_AUTHORITATIVE');
|
|
});
|
|
|
|
test('rollback ON -> OFF disables future writes and touches nothing else', () => {
|
|
setPersistent('mlb');
|
|
expect(mode.isEnabled('mlb')).toBe(true);
|
|
setPersistent(null);
|
|
expect(mode.isEnabled('mlb')).toBe(false);
|
|
expect(mode.evaluate().mode).toBe(mode.WRITE_MODE.OFF);
|
|
});
|
|
});
|
|
|
|
describe('canary and persistent are DIFFERENT modes', () => {
|
|
test('an ambiguous configuration fails closed rather than double-enabling', () => {
|
|
// The two instructions disagree about WHEN WRITING STOPS. We cannot know
|
|
// which the operator meant, so the sport writes nothing.
|
|
const e = mode.evaluate.call(null);
|
|
const fake = {
|
|
lease_state: 'ACTIVE', effective_active_sports: ['mlb'],
|
|
};
|
|
jest.isolateModules(() => {
|
|
jest.doMock('../../src/services/lineageCanaryConfig', () => ({
|
|
LEASABLE_SPORTS: ['mlb'],
|
|
evaluate: () => fake,
|
|
}));
|
|
const m2 = require('../../src/services/lineageWriteMode');
|
|
process.env[m2.ENV_VAR] = 'mlb';
|
|
const ev = m2.evaluate();
|
|
expect(ev.conflict).toBe(m2.CONFLICT.SPORT_IN_BOTH_MODES);
|
|
expect(ev.conflict_sports).toEqual(['mlb']);
|
|
expect(ev.mode).toBe(m2.WRITE_MODE.OFF);
|
|
expect(m2.isEnabled('mlb')).toBe(false);
|
|
delete process.env[m2.ENV_VAR];
|
|
});
|
|
expect(e).toBeDefined();
|
|
});
|
|
|
|
test('a lease alone still enables, with the canary contract untouched', () => {
|
|
jest.isolateModules(() => {
|
|
jest.doMock('../../src/services/lineageCanaryConfig', () => ({
|
|
LEASABLE_SPORTS: ['mlb'],
|
|
evaluate: () => ({ lease_state: 'ACTIVE', effective_active_sports: ['mlb'] }),
|
|
}));
|
|
const m2 = require('../../src/services/lineageWriteMode');
|
|
delete process.env[m2.ENV_VAR];
|
|
expect(m2.evaluate().mode).toBe(m2.WRITE_MODE.CANARY_LEASED);
|
|
expect(m2.isEnabled('mlb')).toBe(true);
|
|
});
|
|
});
|
|
|
|
test('an EXPIRED lease enables nothing — dynamic expiry is preserved', () => {
|
|
jest.isolateModules(() => {
|
|
jest.doMock('../../src/services/lineageCanaryConfig', () => ({
|
|
LEASABLE_SPORTS: ['mlb'],
|
|
evaluate: () => ({ lease_state: 'EXPIRED', effective_active_sports: [] }),
|
|
}));
|
|
const m2 = require('../../src/services/lineageWriteMode');
|
|
delete process.env[m2.ENV_VAR];
|
|
expect(m2.isEnabled('mlb')).toBe(false);
|
|
expect(m2.evaluate().mode).toBe(m2.WRITE_MODE.OFF);
|
|
});
|
|
});
|
|
});
|
|
|
|
/* ────────────────── FAILURE ISOLATION (the product first) ────────────────── */
|
|
describe('lineage failure can never reach authoritative product truth', () => {
|
|
// HERMETIC. Injecting `retention` activates pipeline paths the other snapshot
|
|
// tests never reach, and several resolve a real Supabase client when the
|
|
// environment offers one. Blanking the credentials makes every database
|
|
// dependency a no-op, so this measures the ISOLATION PROPERTY and not the
|
|
// network. Restored afterwards so no other suite inherits it.
|
|
const SUPA_KEYS = ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'SUPABASE_SERVICE_KEY', 'SUPABASE_ANON_KEY'];
|
|
const saved = {};
|
|
beforeAll(() => { for (const k of SUPA_KEYS) { saved[k] = process.env[k]; delete process.env[k]; } });
|
|
afterAll(() => { for (const k of SUPA_KEYS) { if (saved[k] === undefined) delete process.env[k]; else process.env[k] = saved[k]; } });
|
|
|
|
const memCache = () => { const store = {}; return { store, cacheGet: async (k) => (k in store ? store[k] : null), cacheSet: async (k, v) => { store[k] = v; } }; };
|
|
const sampleProps = [{ player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, over_odds: -115, under_odds: -105, book: 'draftkings' }];
|
|
const grades = [{ player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, direction: 'over', grade: 'A+', confidence: 80 }];
|
|
const deps = (cache, extra = {}) => ({
|
|
getOdds: async () => ({ sport: 'mlb', props: sampleProps, provider: 'propline' }),
|
|
// Fires the retention hook the real grader fires. Without it the collector
|
|
// stays empty, `persistedRows` stays null and the lineage gate is never
|
|
// reached — which would make every assertion below vacuously true.
|
|
gradeAndCacheSlate: async (_s, _p, opts) => {
|
|
if (typeof opts.onGraded === 'function') {
|
|
opts.onGraded(
|
|
{
|
|
player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, book: 'draftkings',
|
|
game_time: '2026-08-31T23:05:00Z', game_date: '2026-08-31',
|
|
canonical_event_id: 'mlb:gamepk:999001',
|
|
},
|
|
[
|
|
{ player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, direction: 'over', grade: 'A+', confidence: 80 },
|
|
{ player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, direction: 'under', grade: 'D', confidence: 40 },
|
|
],
|
|
);
|
|
}
|
|
await opts.cacheSet('grades:x', { grades });
|
|
return { written: true, count: 1 };
|
|
},
|
|
resolveStats: async () => ({ found: false }),
|
|
classify: require('../../src/services/archetypeService').classify,
|
|
cacheGet: cache.cacheGet, cacheSet: cache.cacheSet,
|
|
now: () => '2026-08-31T01:00:00Z', nowMs: () => 1000,
|
|
...extra,
|
|
});
|
|
|
|
test('a throwing lineage commit leaves the slate published and the run ok', async () => {
|
|
setPersistent('mlb');
|
|
const cache = memCache();
|
|
// The counter is the point. Without it this test passes when the lineage
|
|
// path is never entered at all, which asserts nothing about isolation.
|
|
let commits = 0;
|
|
const r = await svc.runSnapshot('mlb', deps(cache, {
|
|
retention: {
|
|
persist: async () => ({ attempted: 1, written: 1 }),
|
|
commitPublication: async () => { commits += 1; throw new Error('lineage exploded'); },
|
|
rowsFromSides: retention.rowsFromSides,
|
|
createCollector: retention.createCollector,
|
|
newSnapshotId: retention.newSnapshotId,
|
|
},
|
|
}));
|
|
expect(commits).toBe(1); // the failure really happened
|
|
expect(r.status).toBe('ok'); // and the run still completed
|
|
// The authoritative product write happened regardless...
|
|
expect(cache.store['snapshot:mlb:latest']).toBeTruthy();
|
|
// ...and so did every step AFTER the lineage block. This is what separates
|
|
// "the slate was already written" from "the exception was contained".
|
|
expect(cache.store['grades:mlb']).toBeTruthy();
|
|
});
|
|
|
|
test('a throwing lineage commit does not stop the Ledger write', async () => {
|
|
setPersistent('mlb');
|
|
const cache = memCache();
|
|
const ledgerCalls = [];
|
|
let commits = 0;
|
|
await svc.runSnapshot('mlb', deps(cache, {
|
|
retention: {
|
|
persist: async () => ({ attempted: 1, written: 1 }),
|
|
commitPublication: async () => { commits += 1; throw new Error('lineage exploded'); },
|
|
rowsFromSides: retention.rowsFromSides,
|
|
createCollector: retention.createCollector,
|
|
newSnapshotId: retention.newSnapshotId,
|
|
},
|
|
ledger: {
|
|
recordPipelineGrades: async (...a) => { ledgerCalls.push(a); return { written: 1 }; },
|
|
captureClosing: async () => ({}),
|
|
},
|
|
}));
|
|
expect(commits).toBe(1);
|
|
expect(ledgerCalls.length).toBe(1);
|
|
// read_id linkage is absent, never guessed.
|
|
expect(ledgerCalls[0][3].lineageIndex).toBeNull();
|
|
});
|
|
|
|
test('with the mode OFF no lineage commit is attempted at all', async () => {
|
|
setPersistent(null);
|
|
const cache = memCache();
|
|
let commits = 0;
|
|
await svc.runSnapshot('mlb', deps(cache, {
|
|
retention: {
|
|
persist: async () => ({ attempted: 1, written: 1 }),
|
|
commitPublication: async () => { commits += 1; return { published: 0, candidates: 0 }; },
|
|
rowsFromSides: retention.rowsFromSides,
|
|
createCollector: retention.createCollector,
|
|
newSnapshotId: retention.newSnapshotId,
|
|
},
|
|
}));
|
|
expect(commits).toBe(0);
|
|
});
|
|
});
|
|
|
|
/* ─────────────── PARTIAL-LINEAGE ATOMICITY (no half actions) ─────────────── */
|
|
describe('a failed lineage attempt writes no half action', () => {
|
|
test('a throwing family lookup leaves every row completely blank', async () => {
|
|
const rows = [
|
|
{ sport: 'mlb', game_date: '2026-08-31', player_key: 'a b', stat: 'hits', side: 'over', line: 0.5, published: true },
|
|
{ sport: 'mlb', game_date: '2026-08-31', player_key: 'c d', stat: 'hits', side: 'over', line: 0.5, published: true },
|
|
];
|
|
const out = await retention.attachLineage(rows, { fetchExisting: async () => { throw new Error('boom'); } });
|
|
expect(out.error).toMatch(/boom/);
|
|
for (const r of rows) {
|
|
expect(retention.isValidLineageAction(r)).toBe(false);
|
|
for (const k of retention.LINEAGE_KEYS) expect(r[k]).toBeNull();
|
|
}
|
|
// The natural key stamped before the lookup is cleared too — it is family
|
|
// identity, and a failed attempt has no claim to it.
|
|
expect(rows[0].read_natural_key).toBeNull();
|
|
expect(out.incomplete_cleared).toBe(2);
|
|
});
|
|
});
|
|
|
|
/* ───────────── INDEPENDENT COVERAGE OBSERVER (never trust the writer) ───────────── */
|
|
describe('coverage observer', () => {
|
|
const row = (o) => ({
|
|
sport: 'mlb', game_date: '2026-08-31', player_key: 'aaron judge', stat: 'hits',
|
|
side: 'over', line: 0.5, published: true, ...o,
|
|
});
|
|
const action = (o) => ({
|
|
read_id: 'r1', read_natural_key: 'k', lineage_action: 'ORIGIN', claim_digest: 'd',
|
|
revision_ordinal: 0, lineage_state: 's', lineage_version: 'lin@1',
|
|
claim_schema_version: 'claim@1', digest_algorithm_version: 'sha256-json-sorted@1', ...o,
|
|
});
|
|
|
|
test('THE DENOMINATOR IGNORES lineage_action — the property, not the spelling', () => {
|
|
const base = [row({ player_key: 'a' }), row({ player_key: 'b' })];
|
|
const withLineage = base.map((r) => ({ ...r, ...action({ read_natural_key: r.player_key }) }));
|
|
const without = base.map((r) => ({ ...r }));
|
|
// Same rows, opposite lineage state, identical expectation.
|
|
expect(coverage.expectedKeys(withLineage).keys.size).toBe(coverage.expectedKeys(without).keys.size);
|
|
expect([...coverage.expectedKeys(withLineage).keys].sort())
|
|
.toEqual([...coverage.expectedKeys(without).keys].sort());
|
|
});
|
|
|
|
test('unpublished rows are not expected to have lineage', () => {
|
|
expect(coverage.expectedKeys([row({ published: false })]).keys.size).toBe(0);
|
|
expect(coverage.expectedKeys([row({ published: null })]).keys.size).toBe(0);
|
|
});
|
|
|
|
test('a row with no derivable identity is counted, not silently dropped', () => {
|
|
const e = coverage.expectedKeys([row({ player_key: '' })]);
|
|
expect(e.keys.size).toBe(0);
|
|
expect(e.unkeyable_rows).toBe(1);
|
|
});
|
|
|
|
test('WRITER NEVER RAN: full slate, zero lineage -> MISSING_COVERAGE', () => {
|
|
const a = coverage.auditRows([row({ player_key: 'a' }), row({ player_key: 'b' })]);
|
|
expect(a.expected_keys).toBe(2);
|
|
expect(a.covered_keys).toBe(0);
|
|
expect(a.health).toBe(coverage.HEALTH.MISSING_COVERAGE);
|
|
});
|
|
|
|
test('zero expected and zero written are DIFFERENT answers', () => {
|
|
const none = coverage.auditRows([row({ published: false })]);
|
|
expect(none.health).toBe(coverage.HEALTH.NO_ELIGIBLE_CLAIMS);
|
|
expect(none.health).not.toBe(coverage.HEALTH.MISSING_COVERAGE);
|
|
});
|
|
|
|
test('partial coverage is detected', () => {
|
|
const rows = [
|
|
{ ...row({ player_key: 'a' }), ...action({ read_natural_key: 'mlb|2026-08-31|a|hits|over|0.500000' }) },
|
|
row({ player_key: 'b' }),
|
|
];
|
|
const a = coverage.auditRows(rows);
|
|
expect(a.health).toBe(coverage.HEALTH.PARTIAL_COVERAGE);
|
|
expect(a.missing_keys).toBe(1);
|
|
expect(a.coverage_pct).toBe(50);
|
|
});
|
|
|
|
test('full coverage is HEALTHY', () => {
|
|
const rows = ['a', 'b'].map((p) => ({
|
|
...row({ player_key: p }),
|
|
...action({ read_natural_key: `mlb|2026-08-31|${p}|hits|over|0.500000`, read_id: `r-${p}` }),
|
|
}));
|
|
const a = coverage.auditRows(rows);
|
|
expect(a.health).toBe(coverage.HEALTH.HEALTHY);
|
|
expect(a.coverage_pct).toBe(100);
|
|
expect(a.missing_keys).toBe(0);
|
|
expect(a.extra_keys).toBe(0);
|
|
});
|
|
|
|
test('an invalid partial action is INVALID_GRAPH, never counted as covered', () => {
|
|
const partial = { ...row({ player_key: 'a' }), read_natural_key: 'k', read_id: 'r' }; // 2 of 9
|
|
const a = coverage.auditRows([partial]);
|
|
expect(a.invalid_partial_actions).toBe(1);
|
|
expect(a.covered_keys).toBe(0);
|
|
expect(a.health).toBe(coverage.HEALTH.INVALID_GRAPH);
|
|
});
|
|
|
|
test('extra lineage — covered but never expected — is a defect', () => {
|
|
const rows = [{ ...row({ published: false, player_key: 'a' }), ...action({ read_natural_key: 'ghost' }) }];
|
|
const a = coverage.auditRows(rows);
|
|
expect(a.expected_keys).toBe(0);
|
|
expect(a.extra_keys).toBe(1);
|
|
expect(a.health).toBe(coverage.HEALTH.INVALID_GRAPH);
|
|
});
|
|
|
|
test('a forked chain is a graph defect', () => {
|
|
const k = 'mlb|2026-08-31|a|hits|over|0.500000';
|
|
const rows = [
|
|
{ ...row({ player_key: 'a' }), ...action({ read_natural_key: k, revision_ordinal: 0 }) },
|
|
{ ...row({ player_key: 'a' }), ...action({ read_natural_key: k, revision_ordinal: 0 }) },
|
|
];
|
|
const a = coverage.auditRows(rows);
|
|
expect(a.graph_defects).toBeGreaterThan(0);
|
|
expect(a.health).toBe(coverage.HEALTH.INVALID_GRAPH);
|
|
});
|
|
|
|
test('the observer\'s valid-action fields match the writer\'s, without importing it', () => {
|
|
expect([...coverage.VALID_ACTION_FIELDS].sort())
|
|
.toEqual([...retention.VALID_LINEAGE_ACTION_FIELDS].sort());
|
|
});
|
|
|
|
test('a cohort spanning two game_dates is audited whole, not one slice', () => {
|
|
// The real 2026-08-31 03:00Z shape: a late slot spanning midnight ET. Rows
|
|
// from BOTH dates are eligible and both must be counted, or a date slice
|
|
// with no lineage stays invisible behind a HEALTHY verdict.
|
|
const mk = (d, p, covered) => ({
|
|
sport: 'mlb', game_date: d, player_key: p, stat: 'hits', side: 'over',
|
|
line: 0.5, published: true,
|
|
...(covered ? {
|
|
read_id: `r-${p}`, read_natural_key: `mlb|${d}|${p}|hits|over|0.500000`,
|
|
lineage_action: 'ORIGIN', claim_digest: 'd', revision_ordinal: 0,
|
|
lineage_state: 's', lineage_version: 'lin@1', claim_schema_version: 'claim@1',
|
|
digest_algorithm_version: 'sha256-json-sorted@1',
|
|
} : {}),
|
|
});
|
|
const a = coverage.auditRows([mk('2026-08-31', 'a', true), mk('2026-08-30', 'b', false)]);
|
|
expect(a.expected_keys).toBe(2);
|
|
expect(a.covered_keys).toBe(1);
|
|
expect(a.health).toBe(coverage.HEALTH.PARTIAL_COVERAGE);
|
|
});
|
|
|
|
test('an unavailable audit is not a coverage claim', () => {
|
|
expect(coverage.classify({ audit_available: false })).toBe(coverage.HEALTH.AUDIT_UNAVAILABLE);
|
|
});
|
|
});
|
|
|
|
/* ───────────────────────── ADDITIVE ANCESTRY ───────────────────────── */
|
|
describe('ancestry surface tells the truth about what it has', () => {
|
|
const A = ancestry.ANCESTRY_STATE;
|
|
const act = (o) => ({
|
|
id: 1, read_id: 'r1', read_natural_key: 'k', lineage_action: 'ORIGIN', claim_digest: 'd0',
|
|
revision_ordinal: 0, lineage_state: 'PUBLISHED', lineage_version: 'lin@1',
|
|
claim_schema_version: 'claim@1', digest_algorithm_version: 'sha256-json-sorted@1',
|
|
publication_id: 'p1', ...o,
|
|
});
|
|
|
|
test('a row lineage never ran for is LEGACY_UNVERIFIED, and nothing is synthesized', () => {
|
|
const out = ancestry.classifyAncestry([{ id: 1, publication_id: null, grade: 'B' }]);
|
|
expect(out.state).toBe(A.LEGACY_UNVERIFIED);
|
|
expect(out.revisions).toEqual([]);
|
|
expect(out.read_id).toBeUndefined();
|
|
});
|
|
|
|
test('a row lineage RAN for but did not complete is LINEAGE_UNAVAILABLE, not legacy', () => {
|
|
// This is the shape of the 1,119 historical partial rows.
|
|
const out = ancestry.classifyAncestry([{ id: 1, publication_id: 'p1', read_natural_key: 'k' }]);
|
|
expect(out.state).toBe(A.LINEAGE_UNAVAILABLE);
|
|
expect(out.state).not.toBe(A.LEGACY_UNVERIFIED);
|
|
});
|
|
|
|
test('a partial row is never elected as an ancestry head', () => {
|
|
const out = ancestry.classifyAncestry([
|
|
{ id: 1, publication_id: 'p1', read_natural_key: 'k', read_id: 'r1' },
|
|
]);
|
|
expect(out.state).toBe(A.LINEAGE_UNAVAILABLE);
|
|
expect(out.current).toBeUndefined();
|
|
});
|
|
|
|
test('ORIGIN alone reads as a one-entry chronology', () => {
|
|
const out = ancestry.classifyAncestry([act({})]);
|
|
expect(out.state).toBe(A.LINEAGE_AVAILABLE);
|
|
expect(out.revision_count).toBe(0);
|
|
expect(out.original.action).toBe('ORIGIN');
|
|
expect(out.current.action).toBe('ORIGIN');
|
|
expect(out.authority).toBe('NON_AUTHORITATIVE');
|
|
});
|
|
|
|
test('REVISION ancestry names the exact parent and order', () => {
|
|
const out = ancestry.classifyAncestry([
|
|
act({ id: 1, revision_ordinal: 0, lineage_action: 'ORIGIN' }),
|
|
act({ id: 2, revision_ordinal: 1, lineage_action: 'REVISION', supersedes_id: 1, claim_digest: 'd1', change_type: 'MIXED_CHANGE' }),
|
|
]);
|
|
expect(out.state).toBe(A.LINEAGE_AVAILABLE);
|
|
expect(out.revision_count).toBe(1);
|
|
expect(out.current.revision_ordinal).toBe(1);
|
|
expect(out.current.supersedes_row_id).toBe(1);
|
|
expect(out.current.change_type).toBe('MIXED_CHANGE');
|
|
});
|
|
|
|
test('RECAPTURE is reported as a recapture, never as a revision', () => {
|
|
const out = ancestry.classifyAncestry([
|
|
act({ id: 1, lineage_action: 'ORIGIN', revision_ordinal: 0 }),
|
|
act({ id: 2, lineage_action: 'RECAPTURE', revision_ordinal: 0, recaptures_id: 1 }),
|
|
]);
|
|
expect(out.state).toBe(A.LINEAGE_AVAILABLE);
|
|
expect(out.revision_count).toBe(0);
|
|
expect(out.recapture_count).toBe(1);
|
|
expect(out.recaptures[0].action).toBe('RECAPTURE');
|
|
});
|
|
|
|
test('a broken chain REFUSES rather than electing a head', () => {
|
|
const out = ancestry.classifyAncestry([
|
|
act({ id: 1, lineage_action: 'ORIGIN', revision_ordinal: 0 }),
|
|
act({ id: 2, lineage_action: 'REVISION', revision_ordinal: 1, supersedes_id: 999 }),
|
|
]);
|
|
expect(out.state).toBe(A.INVALID_LINEAGE);
|
|
expect(out.revisions).toEqual([]);
|
|
});
|
|
|
|
test('two read_ids under one natural key is INVALID_LINEAGE', () => {
|
|
const out = ancestry.classifyAncestry([act({ id: 1, read_id: 'r1' }), act({ id: 2, read_id: 'r2' })]);
|
|
expect(out.state).toBe(A.INVALID_LINEAGE);
|
|
});
|
|
|
|
test('the projection carries only what lineage knows', () => {
|
|
const p = ancestry.project({ ...act({}), p_win: 0.6, grade: 'A', locked_odds: '-110', outcome: 'hit' });
|
|
for (const forbidden of ['p_win', 'grade', 'locked_odds', 'outcome', 'projection']) {
|
|
expect(p[forbidden]).toBeUndefined();
|
|
}
|
|
expect(p.read_id).toBe('r1');
|
|
expect(p.claim_digest).toBe('d0');
|
|
});
|
|
|
|
test('a valid chain with a stamped-but-unrecorded sibling discloses the hole', () => {
|
|
// The production shape: one good ORIGIN plus a later publication that was
|
|
// attempted and never recorded. The chain stays valid and the partial row
|
|
// stays out of it, but the answer must not claim the chronology is whole.
|
|
const out = ancestry.classifyAncestry([
|
|
act({ id: 1, publication_id: 'p1' }),
|
|
{ id: 2, publication_id: 'p2', read_natural_key: 'k' },
|
|
]);
|
|
expect(out.state).toBe(A.LINEAGE_AVAILABLE);
|
|
expect(out.revisions).toHaveLength(1);
|
|
expect(out.revisions[0].snapshot_row_id).toBe(1); // the partial is excluded
|
|
expect(out.chronology_complete).toBe(false);
|
|
expect(out.unrecorded_publications).toBe(1);
|
|
});
|
|
|
|
test('a chain with no unrecorded siblings is complete', () => {
|
|
const out = ancestry.classifyAncestry([act({ id: 1, publication_id: 'p1' })]);
|
|
expect(out.chronology_complete).toBe(true);
|
|
expect(out.unrecorded_publications).toBe(0);
|
|
});
|
|
|
|
test('nothing found is NOT_FOUND, which is not an absence of lineage', () => {
|
|
expect(ancestry.classifyAncestry([]).state).toBe(A.NOT_FOUND);
|
|
});
|
|
});
|
|
|
|
/* ────────────────────── PRESERVATION ────────────────────── */
|
|
describe('this tranche changes nothing it was told not to', () => {
|
|
const fs = require('fs'); const path = require('path');
|
|
const ROOT = path.resolve(__dirname, '..', '..');
|
|
|
|
test('the grade-shift badge is untouched — no lineage anywhere near it', () => {
|
|
for (const f of ['src/routes/ledger.js', 'src/routes/profiles.js']) {
|
|
const src = fs.readFileSync(path.join(ROOT, f), 'utf8');
|
|
expect(src).toMatch(/revised_from_grade/);
|
|
expect(src).not.toMatch(/lineage|read_id/);
|
|
}
|
|
});
|
|
|
|
test('retention identity and its collision counter are unchanged', () => {
|
|
expect(retention.RETENTION_CONFLICT.split(',')).toEqual(
|
|
['snapshot_id', 'game_id', 'canonical_event_id', 'player_key', 'stat', 'line', 'side']);
|
|
const dup = [
|
|
{ snapshot_id: 's', game_id: 'g', canonical_event_id: 'e', player_key: 'a', stat: 'hits', line: 0.5, side: 'over' },
|
|
{ snapshot_id: 's', game_id: 'g', canonical_event_id: 'e', player_key: 'a', stat: 'hits', line: 0.5, side: 'over' },
|
|
];
|
|
expect(retention.expectedMaterialization(dup).collision_count).toBe(1);
|
|
});
|
|
|
|
test('participant identity guards are unchanged', () => {
|
|
const pid = require('../../src/services/model/participantIdentity');
|
|
const a = pid.auditParticipantIdentity([
|
|
{ canonical_event_id: 'e', mlb_person_id: 1, player_key: 'x' },
|
|
{ canonical_event_id: 'e', mlb_person_id: 1, player_key: 'y' },
|
|
]);
|
|
expect(a.participant_identity_split_count).toBe(1);
|
|
expect(pid.semanticPlayerKey({ player: 'Mickey Gasper', mlb_person_id: 681508, canonical_player_name: 'Mickey Gasper' }).canonical).toBe(true);
|
|
});
|
|
|
|
test('the ancestry surface writes nothing', () => {
|
|
const src = fs.readFileSync(path.join(ROOT, 'src/services/read/readAncestry.js'), 'utf8')
|
|
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
|
for (const verb of ['.insert(', '.update(', '.upsert(', '.delete(']) {
|
|
expect(src).not.toContain(verb);
|
|
}
|
|
});
|
|
|
|
test('the coverage observer writes nothing', () => {
|
|
const src = fs.readFileSync(path.join(ROOT, 'src/services/lineageCoverage.js'), 'utf8')
|
|
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
|
for (const verb of ['.insert(', '.update(', '.upsert(', '.delete(']) {
|
|
expect(src).not.toContain(verb);
|
|
}
|
|
});
|
|
});
|