fathom-multi-key.test.ts
Language TypeScript6.5 KB164 lines
/*** Several keys, and what happens when one of them is dark.** Fathom has no org keys; each key returns its owner's recordings plus* whatever Fathom shares with that user. Adding keys buys coverage, never* gating (config/fathom-keys.ts).** With more than one key there are two ways to lie, and both break the prime* invariant:** - all-or-nothing failure. One revoked key blanks meetings for everyone,* throwing away the other keys' real data to report one.* - a swallowed per-key failure, which is worse. A key that cannot reach* someone's meetings then looks exactly like a person who recorded none,* and one of those two means free capacity.** So the rule asserted here: a partial read stays partial. The data survives,* and every surface that shows it says which key is dark, by owner (report* what happened, name no cause).*/import { describe, expect, it } from 'vitest'import { FIXTURE_SNAPSHOT } from '../src/fixtures.js'import { headline, peopleBoard, type Snapshot, type FathomSourceStatus } from '../src/collect.js'import { sourceStatuses, myDay, personViews } from '../src/view-model.js'import { render } from '../src/render.js'import { viewerFor } from '../src/viewer.js'import { FATHOM_KEYS } from '../../../config/fathom-keys.js'/*** `mode: 'live'` because sourceStatuses() deliberately overrides every pill to* "sample data" on a fixture snapshot, so a fixture never renders `live` pills* over frozen data. These tests are about which live state the* Fathom pill reaches, so they have to be on the live side of that override.*/const snap: Snapshot = { ...FIXTURE_SNAPSHOT, mode: 'live', fixtureCapturedAt: null }const withSources = (fathomSources: FathomSourceStatus[]): Snapshot => ({ ...snap, fathomSources })const fathomPill = (s: Snapshot) => sourceStatuses(s).find((r) => r.name === 'Fathom')!const DARK_INES: FathomSourceStatus = {owner: 'Ines Carvalho',ok: false,error: 'HTTP 401',returned: 0,truncated: false,}describe('the config still says what this suite assumes', () => {it('lists Ines and Theo alongside Sam', () => {const owners = FATHOM_KEYS.map((k) => k.owner)expect(owners).toContain('Sam Whitaker')expect(owners).toContain('Ines Carvalho')expect(owners).toContain('Theo Bassett')})it('is a hand-written list, not a loop over the roster', () => {// Four people have handed over no key. If this ever equals the roster// length, somebody has generated the list and the deliberate gap is gone.expect(FATHOM_KEYS.length).toBe(3)})it('names an environment variable per owner and never an inline secret', () => {for (const k of FATHOM_KEYS) {expect(k.secret).toMatch(/^FATHOM_KEY_[A-Z]+$/)}})})describe('every key clean', () => {it('reads live and names the keys it read', () => {const pill = fathomPill(snap)expect(pill.status).toBe('live')expect(pill.tone).toBe('live')expect(pill.note).toContain('Ines Carvalho')expect(pill.note).toContain('Theo Bassett')})it('says a person at zero meetings is a floor, not a fact', () => {// A gap that stands in for knowledge is surfaced every run (rule 2).expect(fathomPill(snap).note).toMatch(/floor, not a fact/)})})describe('one dark key degrades the source without erasing it', () => {const partial = withSources([{ owner: 'Sam Whitaker', ok: true, error: null, returned: 8, truncated: false },DARK_INES,{ owner: 'Theo Bassett', ok: true, error: null, returned: 2, truncated: false },])it('is partial, not live and not failed', () => {expect(fathomPill(partial).status).toBe('partial')expect(fathomPill(partial).tone).toBe('degraded')})it('names the owner whose key failed, and the failure verbatim', () => {const note = fathomPill(partial).noteexpect(note).toContain('Ines Carvalho')expect(note).toContain('HTTP 401')})it('states no cause for the failure', () => {// The 401 handler once said a token had "most likely expired" about a token// with eleven months left. Report the status; do not pick a reason.expect(fathomPill(partial).note).not.toMatch(/expired|revoked|most likely/i)})it('keeps every meeting the working keys returned', () => {// The whole point. Two keys' data must not be discarded to report a third.expect(partial.meetings.ok).toBe(true)expect(partial.meetings.data).toEqual(snap.meetings.data)})})describe('a truncated sweep is reported, not silently short', () => {it('reads partial and says the window is incomplete', () => {// Keep what was fetched AND say it was capped.const capped = withSources([{ owner: 'Sam Whitaker', ok: true, error: null, returned: 400, truncated: true },{ owner: 'Ines Carvalho', ok: true, error: null, returned: 3, truncated: false },{ owner: 'Theo Bassett', ok: true, error: null, returned: 2, truncated: false },])expect(fathomPill(capped).status).toBe('partial')expect(fathomPill(capped).note).toMatch(/page cap/)expect(fathomPill(capped).note).toContain('Sam Whitaker')})})describe("a person's own card says a key is dark", () => {const partial = withSources([{ owner: 'Sam Whitaker', ok: true, error: null, returned: 8, truncated: false },DARK_INES,{ owner: 'Theo Bassett', ok: true, error: null, returned: 2, truncated: false },])const viewFor = (s: Snapshot, name: string) =>personViews(peopleBoard(s), s.tasks.data).find((v) => v.name === name)!it('sets commitmentsPartial when a key failed', () => {const d = myDay(partial, viewFor(partial, 'Sam Whitaker'), viewerFor('hello@meridiandisplay.example'))expect(d.commitmentsTracked).toBe(true)expect(d.commitmentsPartial).toContain('Ines Carvalho')})it('leaves it null when every key is clean', () => {const d = myDay(snap, viewFor(snap, 'Sam Whitaker'), viewerFor('hello@meridiandisplay.example'))expect(d.commitmentsPartial).toBeNull()})it('reaches the rendered page, not just the view-model', () => {// A card that is short by one key looks exactly like a light fortnight, and// nobody scrolls to the source band to find out which. The finished document// is the only thing that proves the sentence is actually on it.const html = render(partial,headline(partial),[],'2026-08-12',null,viewerFor('hello@meridiandisplay.example'),)expect(html).toContain('Ines Carvalho')expect(html).toMatch(/could not be read this run/)})})