/** * Durable offline write-queue store (#33). The invariant mirrors the snapshot * store's purity: the queue file is a convenience mirror, and a missing or corrupt * file must degrade to "empty queue", never a crash — losing a queued write is * bad, but crashing the whole write path is worse. These tests also exercise the * store together with core's coalescing/replay so the round-trip an offline edit * takes (enqueue → persist → reload → replay → drain) is covered end to end. */ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { enqueueWrite, replayQueue, type IssueChange } from '@commitea/core' 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 { loadQueue, saveQueue } from './queue-store.js' const estChange = (issue: number, estimate = 'est/5d'): IssueChange => ({ kind: 'reestimate', issue, estimate }) as IssueChange const assignChange = (issue: number, assignee: string | null): IssueChange => ({ kind: 'assign', issue, assignee }) as IssueChange describe('queue-store (#33)', () => { let dir: string | null = null const path = () => { if (!dir) dir = mkdtempSync(join(tmpdir(), 'commitea-queue-')) return join(dir, 'commitea-write-queue.json') } afterEach(() => { if (dir) rmSync(dir, { recursive: true, force: true }) dir = null }) it('round-trips a persisted queue', () => { const p = path() const queue = enqueueWrite([], estChange(1), '2026-07-11T00:00:00Z') saveQueue(queue, p) expect(loadQueue(p)).toEqual(queue) }) it('a missing file degrades to an empty queue (never a crash)', () => { // nothing written yet — load must return [] so the write path stays alive expect(loadQueue(path())).toEqual([]) }) it('a corrupt or wrong-shape file is treated as an empty queue', () => { const p = path() writeFileSync(p, '{ not json', 'utf8') expect(loadQueue(p)).toEqual([]) // valid JSON but not an array (drift) is also rejected writeFileSync(p, JSON.stringify({ nope: true }), 'utf8') expect(loadQueue(p)).toEqual([]) }) it('persisted queue coalesces by (issue, axis) across saves — the later intent wins', () => { const p = path() // two estimates for the same issue, queued while offline: only the last survives saveQueue(enqueueWrite(loadQueue(p), estChange(1, 'est/2d'), '2026-07-11T00:00:00Z'), p) saveQueue(enqueueWrite(loadQueue(p), estChange(1, 'est/8d'), '2026-07-11T00:01:00Z'), p) // a different axis on the same issue coexists saveQueue(enqueueWrite(loadQueue(p), assignChange(1, 'ana'), '2026-07-11T00:02:00Z'), p) const queue = loadQueue(p) expect(queue).toHaveLength(2) expect(queue.map((w) => w.change)).toEqual([estChange(1, 'est/8d'), assignChange(1, 'ana')]) }) it('reloads and replays the queue, draining what applies and keeping what fails', async () => { const p = path() saveQueue(enqueueWrite(loadQueue(p), estChange(1), '2026-07-11T00:00:00Z'), p) saveQueue(enqueueWrite(loadQueue(p), assignChange(2, 'ana'), '2026-07-11T00:01:00Z'), p) // Simulate reconnect: replay through an apply that fails only for issue #2 // (still unreachable / rejected). The successful write drains; the other stays. const { drained, remaining } = await replayQueue(loadQueue(p), async (c) => c.issue === 2 ? { ok: false } : { ok: true }, ) saveQueue(remaining, p) expect(drained.map((w) => w.change.issue)).toEqual([1]) expect(loadQueue(p).map((w) => w.change.issue)).toEqual([2]) }) })