/** * Purity / rebuild guarantee for the *shipped* durable cache (#30, D4). The app's * on-disk reconcile cache is this JSON snapshot-store. The invariant: delete it * and lose nothing — the durable truth is in gitea, the file is only a boot/offline * mirror. This test deletes the real file and asserts the store degrades to * "no cache" (null), which is what forces the next `getSnapshot` to reconcile * fresh from gitea rather than serve stale or missing data. */ import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' // electron can't be imported outside the Electron runtime; the store only needs // app.getPath for its default path, which every test overrides with an injected path. vi.mock('electron', () => ({ app: { getPath: () => tmpdir() } })) import { loadSnapshot, type PersistedSnapshot, saveSnapshot } from './snapshot-store.js' const SNAP: Omit = { issues: [{ number: 1, title: 'An issue', state: 'open', labels: ['est/5d', 'p/1'] }], milestones: [{ id: 7, title: 'P2' }], deps: [{ issue: 1, dependsOn: 2 }], timelines: { 1: [{ type: 'opened', at: '2026-07-01T00:00:00Z' }] }, } describe('snapshot-store purity (#30)', () => { let dir: string | null = null const path = () => { if (!dir) dir = mkdtempSync(join(tmpdir(), 'commitea-snap-')) return join(dir, 'commitea-snapshot.json') } afterEach(() => { if (dir) rmSync(dir, { recursive: true, force: true }) dir = null }) it('round-trips a saved snapshot', () => { const p = path() saveSnapshot(SNAP, '2026-07-09T00:00:00Z', p) const back = loadSnapshot(p) expect(back).toEqual({ ...SNAP, savedAt: '2026-07-09T00:00:00Z' }) }) it('delete the cache file → load returns null (degrades to no-cache, not a crash)', () => { const p = path() saveSnapshot(SNAP, '2026-07-09T00:00:00Z', p) expect(loadSnapshot(p)).not.toBeNull() rmSync(p) // delete the durable cache expect(existsSync(p)).toBe(false) // The store must NOT throw and must report "no cache" so the next reconcile // rebuilds from gitea. If this ever returned stale data or threw, D4 breaks. expect(loadSnapshot(p)).toBeNull() }) it('a corrupt / partial file is treated as no-cache, never a crash', () => { const p = path() writeFileSync(p, '{ this is not json', 'utf8') expect(loadSnapshot(p)).toBeNull() // valid JSON but wrong shape (no issues array) is also rejected writeFileSync(p, JSON.stringify({ savedAt: 'x' }), 'utf8') expect(loadSnapshot(p)).toBeNull() }) it('re-saving after a delete restores the cache — the rebuild is lossless', () => { const p = path() saveSnapshot(SNAP, '2026-07-09T00:00:00Z', p) const before = loadSnapshot(p) rmSync(p) expect(loadSnapshot(p)).toBeNull() // a resync would call saveSnapshot again with the freshly reconciled data saveSnapshot(SNAP, '2026-07-09T01:00:00Z', p) const after = loadSnapshot(p) // same durable payload, only the savedAt marker differs expect(after!.issues).toEqual(before!.issues) expect(after!.milestones).toEqual(before!.milestones) expect(after!.deps).toEqual(before!.deps) expect(after!.timelines).toEqual(before!.timelines) }) })