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>
38 lines
1.8 KiB
TypeScript
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()
|
|
})
|
|
})
|