Files
commitea/apps/desktop/e2e/live-persistence.spec.ts
Croissant Le Doux 2f6636684e feat: persist the reconcile cache to disk — instant boot + offline reads
Extends the in-memory cache into a durable mirror. The reconcile snapshot is
written to disk on every successful reconcile; on boot the app shows it instantly
(stale-while-revalidate) instead of a blank board, and if gitea is unreachable,
reads fall back to it (offline). Rebuildable — the durable truth stays in gitea.

- snapshot-store.ts: load/save the snapshot as JSON in app userData (never throws;
  corrupt/absent → "no cache"). At this scale (~34 issues, 37KB) the whole snapshot
  fits in memory, so a JSON file beats indexed SQL — no query benefit yet, no
  native-module (better-sqlite3/electron-rebuild) or WASM dependency. That's the
  next step if the mirror ever needs indexed queries over larger data.
- gitea.ts: getSnapshot persists on a fresh pull; bootSnapshot() returns the
  persisted snapshot (without seeding the cache — agents still reconcile fresh);
  gitea:boot serves it; gitea:reconcile falls back to it on failure (stale:true).
- useBacklog: stale-while-revalidate — boot instantly, then a fresh reconcile
  supersedes; a reconcile error keeps the shown snapshot instead of erroring.

Verified: desktop typecheck clean, 14 fixture e2e green. Live: the snapshot
persists (34 issues / 44 deps / 34 timelines / 5 milestones written to disk); a
second launch with gitea unreachable renders the full real board — NOW/NEXT/LATER
+ the Monte Carlo cone — entirely from the cache (new live-persistence e2e).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 23:43:44 -04:00

38 lines
1.8 KiB
TypeScript

import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { _electron as electron, expect, test } from '@playwright/test'
const here = dirname(fileURLToPath(import.meta.url))
const MAIN = join(here, '..', 'out', 'main', 'index.js')
// Opt-in (GITEA_LIVE=1). A real launch persists the snapshot; a second launch with
// gitea unreachable must still show the board + real scheduler output from the
// persisted cache (offline reads). No model needed.
test.describe('live persistence', () => {
test('offline: serves the persisted snapshot', async () => {
test.skip(!process.env.GITEA_LIVE, 'GITEA_LIVE not set — opt-in live test')
test.setTimeout(120_000)
// launch 1 — real reconcile writes the snapshot to disk
const app1 = await electron.launch({ args: [MAIN], env: { ...process.env } })
const w1 = await app1.firstWindow()
await w1.waitForLoadState('domcontentloaded')
// a scheduler-only phrase confirms real data reconciled (never emitted by fixtures)
await expect(w1.getByText(/on the critical path|unblocks #|waits on #|· ready/).first()).toBeVisible({ timeout: 30000 })
await app1.close()
// launch 2 — gitea unreachable; the reconcile must fall back to the persisted snapshot
const app2 = await electron.launch({
args: [MAIN],
env: { ...process.env, GITEA_BASE_URL: 'http://127.0.0.1:9' },
})
const w2 = await app2.firstWindow()
await w2.waitForLoadState('domcontentloaded')
// Focus still renders real scheduler output — proving it came from the cache, offline
await expect(w2.getByText(/on the critical path|unblocks #|waits on #|· ready/).first()).toBeVisible({ timeout: 20000 })
await w2.screenshot({ path: join(here, '.artifacts', 'screens', 'live-offline.png'), fullPage: true, animations: 'disabled' })
await app2.close()
})
})