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>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user