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>
This commit is contained in:
37
apps/desktop/e2e/live-persistence.spec.ts
Normal file
37
apps/desktop/e2e/live-persistence.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user