1bdc3baec7
Measured against real production history while validating the ancestry surface: `aj ewing / total_bases / over / 0.5` on 2026-08-28 carries a valid ORIGIN from the 18:10 cohort AND a row from the 19:03 cohort that was stamped for publication and never recorded an action. The chain is genuinely valid and the partial row is correctly excluded from it — that part of the contract held. But answering LINEAGE_AVAILABLE and nothing else presents an incomplete chronology as a complete one, which is the exact failure this surface exists to make visible, one grain finer than a whole Read. The response now carries `chronology_complete` and `unrecorded_publications`. The state is unchanged, because the chain that exists really is valid; what changes is that the gap is disclosed instead of smoothed over. Suite 397/5,498/0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
524 lines
24 KiB
JavaScript
524 lines
24 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('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);
|
|
}
|
|
});
|
|
});
|