/** * READ HISTORY — the product surface for the one truth lineage owns. * * The load-bearing separation this suite defends: a lineage REVISION means the * published CLAIM changed; the grade strike means the LETTER changed. They must * never look, read, or be styled like the same event. */ const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..', '..'); const rh = require('../../web/src/lib/readHistory'); const ancestry = require('../../src/services/read/readAncestry'); const COMPONENT = fs.readFileSync(path.join(ROOT, 'web/src/components/vyndr/ReadHistory.tsx'), 'utf8'); const ROUTE = fs.readFileSync(path.join(ROOT, 'src/routes/ancestry.js'), 'utf8'); const LEDGER_PAGE = fs.readFileSync(path.join(ROOT, 'web/src/app/ledger/page.tsx'), 'utf8'); const chain = (over = {}) => ({ ancestry: { state: 'LINEAGE_AVAILABLE', revisions: [ { snapshot_row_id: 1, revision_ordinal: 0, action: 'ORIGIN', change_type: 'INITIAL_PUBLICATION', captured_at: '2026-08-30T01:02:44Z', published_grade: 'C', published_p_win: 0.567, published_line: 4.5 }, { snapshot_row_id: 2, revision_ordinal: 1, action: 'REVISION', change_type: 'MARKET_REPRICE', captured_at: '2026-08-30T03:01:43Z', published_grade: 'C', published_p_win: 0.567, published_line: 4.5 }, { snapshot_row_id: 3, revision_ordinal: 2, action: 'REVISION', change_type: 'BELIEF_CHANGE', captured_at: '2026-08-31T03:47:09Z', published_grade: 'C-', published_p_win: 0.515, published_line: 4.5 }, ], recaptures: [], chronology_complete: true, unrecorded_publications: 0, publications_before_recording: 0, ...over, }, }); describe('the history reads as history, not as a database', () => { test('ORIGIN, REVISION and change types get plain words', () => { const v = rh.mapAncestry(chain()); expect(v.available).toBe(true); expect(v.entries.map((e) => e.label)).toEqual(['First published', 'Updated', 'Updated']); expect(v.entries[1].what).toBe('The price moved'); expect(v.entries[2].what).toBe('The read changed'); }); test('the newest entry is marked current, and it is worded, not only coloured', () => { const v = rh.mapAncestry(chain()); expect(v.entries[2].is_current).toBe(true); expect(v.entries[0].is_current).toBe(false); expect(COMPONENT).toMatch(/CURRENT/); }); test('each entry carries what was PUBLISHED then, from the authoritative row', () => { const v = rh.mapAncestry(chain()); expect(v.entries.map((e) => e.grade)).toEqual(['C', 'C', 'C-']); // Sourced from model_snapshots via the projection, never from lineage metadata. const svc = fs.readFileSync(path.join(ROOT, 'src/services/read/readAncestry.js'), 'utf8'); expect(svc).toMatch(/published_grade: row\.grade/); }); test('no backend vocabulary reaches the user', () => { // Only the RENDERED strings. The lookup keys are ORIGIN/REVISION/RECAPTURE // by design — they are how the code addresses an action, not what a person // reads, and asserting over them tests the wrong thing. const rendered = [ ...Object.values(rh.STATE_COPY).filter(Boolean).flatMap((c) => [c.headline, c.detail]), ...Object.values(rh.ACTION_COPY).map((c) => c.label), ...Object.values(rh.CHANGE_COPY), rh.summariseRecaptures([{}, {}]).label, ].join(' | ').toLowerCase(); for (const w of ['lineage', 'natural key', 'ordinal', 'digest', 'graph', 'recapture', 'origin', 'chronology']) { expect(rendered).not.toContain(w); } }); test('the feature is not called Lineage anywhere a user can see', () => { const visible = COMPONENT.split('\n').filter((l) => !l.trim().startsWith('*') && !l.trim().startsWith('//') && !l.trim().startsWith('/*')); expect(visible.join('\n')).not.toMatch(/>[^<]*[Ll]ineage[^<]* { test('repeated unchanged observations collapse to one honest line', () => { const v = rh.mapAncestry(chain({ recaptures: [{ captured_at: 'a' }, { captured_at: 'b' }, { captured_at: 'c' }] })); expect(v.recaptureSummary.count).toBe(3); expect(v.recaptureSummary.label).toMatch(/Rechecked 3 times with no change/); // The chronology itself is unchanged — nothing was removed from the data. expect(v.entries).toHaveLength(3); }); test('the API still returns every recapture — the UI summarises, the contract does not', () => { const act = (o) => ({ id: 1, 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: 'x', publication_id: 'p', ...o, }); const out = ancestry.classifyAncestry([ act({ id: 1 }), act({ id: 2, lineage_action: 'RECAPTURE' }), act({ id: 3, lineage_action: 'RECAPTURE' }), ]); expect(out.recaptures).toHaveLength(2); expect(out.recapture_count).toBe(2); }); }); describe('truth states', () => { test('legacy says history was not recorded — not that nothing happened', () => { const v = rh.mapAncestry({ ancestry: { state: 'LEGACY_UNVERIFIED' } }); expect(v.available).toBe(false); expect(v.headline).toBe('History not recorded'); expect(v.detail).toMatch(/started keeping Read history after/i); expect(v.entries).toEqual([]); }); test('unavailable is NOT presented as legacy', () => { const v = rh.mapAncestry({ ancestry: { state: 'LINEAGE_UNAVAILABLE' } }); expect(v.headline).toBe('History unavailable'); expect(v.headline).not.toBe(rh.STATE_COPY.LEGACY_UNVERIFIED.headline); expect(v.detail).toMatch(/should have a history/i); }); test('invalid fails closed into a restrained state and names nothing scary', () => { const v = rh.mapAncestry({ ancestry: { state: 'INVALID_LINEAGE' } }); expect(v.available).toBe(false); expect(v.entries).toEqual([]); // The USER-VISIBLE copy, not the machine `state` field the component keys off. const shown = `${v.headline} ${v.detail}`.toLowerCase(); expect(shown).not.toMatch(/corrupt|invalid|fork|defect|integrity/); }); test('an incomplete chronology never claims to be complete, and says WHICH hole', () => { const before = rh.mapAncestry(chain({ chronology_complete: false, publications_before_recording: 2 })); expect(before.incomplete).toBe(true); expect(before.incompleteNote).toMatch(/before VYNDR started recording/i); const failed = rh.mapAncestry(chain({ chronology_complete: false, unrecorded_publications: 1 })); expect(failed.incompleteNote).toMatch(/couldn’t be recorded/i); const both = rh.mapAncestry(chain({ chronology_complete: false, publications_before_recording: 1, unrecorded_publications: 1 })); expect(both.incompleteNote).toMatch(/Some earlier updates aren’t recorded/i); expect(rh.mapAncestry(chain()).incomplete).toBe(false); }); }); describe('THE SEPARATION — claim changed is not letter changed', () => { test('the history never uses the grade strike styling', () => { expect(COMPONENT).not.toMatch(/line-through/); expect(COMPONENT).not.toMatch(/revised_from_grade/); expect(COMPONENT).not.toMatch(/GradePill|gradeShift|GradeShift/); }); test('"Updated" never asserts the grade moved', () => { const v = rh.mapAncestry(chain()); // ordinal 1 is a pure reprice with the SAME letter on both sides. expect(v.entries[1].what).toBe('The price moved'); expect(v.entries[1].grade).toBe(v.entries[0].grade); expect(JSON.stringify(rh.ACTION_COPY).toLowerCase()).not.toMatch(/grade|letter/); }); test('the ledger card still renders the grade strike, untouched', () => { expect(LEDGER_PAGE).toMatch(/revised_from_grade/); expect(LEDGER_PAGE).toMatch(/textDecoration: 'line-through'/); expect(LEDGER_PAGE).toMatch(//); }); }); describe('client, transport and accessibility', () => { test('the component uses the established API client, not ad hoc fetch', () => { expect(COMPONENT).toMatch(/import \{ getReadAncestry \} from '@\/lib\/api'/); const body = COMPONENT.replace(/\/\*[\s\S]*?\*\//g, ''); expect(body).not.toMatch(/\bfetch\(/); expect(fs.readFileSync(path.join(ROOT, 'web/src/lib/api.ts'), 'utf8')).toMatch(/export async function getReadAncestry/); }); test('transport failures are never rendered as history states', () => { for (const code of ['401', '404', '429']) expect(COMPONENT).toContain(code); expect(COMPONENT).toMatch(/transportError/); // The unavailable-state copy is chosen by mapAncestry; transport copy is separate. expect(COMPONENT).toMatch(/History could not be loaded right now/); expect(JSON.stringify(rh.STATE_COPY)).not.toMatch(/could not be loaded/); }); test('the disclosure is keyboard and screen-reader usable', () => { expect(COMPONENT).toMatch(/type="button"/); expect(COMPONENT).toMatch(/aria-expanded=\{open\}/); expect(COMPONENT).toMatch(/aria-controls=\{panelId\}/); expect(COMPONENT).toMatch(/aria-hidden="true"/); // the caret is decorative expect(COMPONENT).toMatch(/role="status"/); expect(COMPONENT).toMatch(/
    { expect(ROUTE).toMatch(/requireAuth/); expect(ROUTE).toMatch(/UUID_RE/); expect(ROUTE).not.toMatch(/read_natural_key/); }); });