Files
AeroFetch/test/reconcile.test.ts
T
debont80 2ef19ead71 feat(audit): Batch 16 — store discipline (L142 reconciliation, L93 view-model hooks)
L142 (+CC14): keyed latest-wins reconciler (lib/reconcile.ts, unit-tested)
applies main's authoritative IPC returns in the history/templates/sources
stores, extending the M34 settings pattern; loadSources shares the key so
a slow list read can't clobber a newer mutation result.

L93 (+CC13): settings cards, TerminalView, and LibraryView now reach IPC
only through colocated view-model hooks (useAboutCard/useBackupCard/
useCookiesCard/useSoftwareUpdateCard/useNetworkCard/useTerminalRun) or
store actions (openHighContrastSettings, scheduled-sync on sources) —
no window.api left in components.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 11:10:49 -04:00

102 lines
3.5 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest'
import { createReconciler } from '../src/renderer/src/lib/reconcile'
/** A promise whose resolution the test controls. */
function deferred<T>(): {
promise: Promise<T>
resolve: (v: T) => void
reject: (e: unknown) => void
} {
let resolve!: (v: T) => void
let reject!: (e: unknown) => void
const promise = new Promise<T>((res, rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
const flush = () => new Promise((r) => setTimeout(r, 0))
describe('createReconciler (L142)', () => {
it('applies the response of a single call', async () => {
const reconcile = createReconciler()
const apply = vi.fn()
const d = deferred<string[]>()
reconcile('k', d.promise, apply, vi.fn())
d.resolve(['a'])
await flush()
expect(apply).toHaveBeenCalledWith(['a'])
})
it('drops a stale response when a newer call was issued on the same key', async () => {
const reconcile = createReconciler()
const applied: string[][] = []
const apply = (v: string[]) => applied.push(v)
const first = deferred<string[]>()
const second = deferred<string[]>()
reconcile('k', first.promise, apply, vi.fn())
reconcile('k', second.promise, apply, vi.fn())
// Even resolving in order, only the newest call's response is applied —
// the first would transiently roll back the second's optimistic update.
first.resolve(['a'])
second.resolve(['a', 'b'])
await flush()
expect(applied).toEqual([['a', 'b']])
})
it('drops an out-of-order late response entirely', async () => {
const reconcile = createReconciler()
const applied: string[][] = []
const first = deferred<string[]>()
const second = deferred<string[]>()
reconcile('k', first.promise, (v) => applied.push(v), vi.fn())
reconcile('k', second.promise, (v) => applied.push(v), vi.fn())
second.resolve(['a', 'b'])
await flush()
first.resolve(['a']) // resolves after the newer response was applied
await flush()
expect(applied).toEqual([['a', 'b']])
})
it('keys are independent — a call on one key never suppresses another', async () => {
const reconcile = createReconciler()
const applied: Record<string, number[]> = {}
const a = deferred<number[]>()
const b = deferred<number[]>()
reconcile('a', a.promise, (v) => (applied.a = v), vi.fn())
reconcile('b', b.promise, (v) => (applied.b = v), vi.fn())
a.resolve([1])
b.resolve([2])
await flush()
expect(applied).toEqual({ a: [1], b: [2] })
})
it('routes rejection to onError and never to apply', async () => {
const reconcile = createReconciler()
const apply = vi.fn()
const onError = vi.fn()
const d = deferred<string[]>()
reconcile('k', d.promise, apply, onError)
d.reject(new Error('ipc failed'))
await flush()
expect(apply).not.toHaveBeenCalled()
expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'ipc failed' }))
})
it('a rejected older call does not block applying the newer response', async () => {
const reconcile = createReconciler()
const applied: string[][] = []
const onError = vi.fn()
const first = deferred<string[]>()
const second = deferred<string[]>()
reconcile('k', first.promise, (v) => applied.push(v), onError)
reconcile('k', second.promise, (v) => applied.push(v), onError)
first.reject(new Error('boom'))
second.resolve(['b'])
await flush()
expect(applied).toEqual([['b']])
expect(onError).toHaveBeenCalledTimes(1)
})
})