2ef19ead71
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>
102 lines
3.5 KiB
TypeScript
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)
|
|
})
|
|
})
|