Files
orca/src/main/ipc/renderer-shutdown-checkpoint.test.ts
T
mirror 34fa2cd5ca
Skill update round trip / roundtrip (false, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, latest) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
source snapshot 2026-09-02
2026-09-02 02:03:16 +08:00

216 lines
7.4 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
const { syncHandlers, invokeHandlers } = vi.hoisted(() => ({
syncHandlers: new Map<
string,
(event: { returnValue?: unknown }, args: Record<string, unknown>) => void
>(),
invokeHandlers: new Map<string, () => Promise<{ ok: boolean }>>()
}))
vi.mock('electron', () => ({
ipcMain: {
on: vi.fn(
(
channel: string,
handler: (event: { returnValue?: unknown }, args: Record<string, unknown>) => void
) => {
syncHandlers.set(channel, handler)
}
),
handle: vi.fn((channel: string, handler: () => Promise<{ ok: boolean }>) => {
invokeHandlers.set(channel, handler)
})
}
}))
import {
registerRendererShutdownCheckpointHandler,
SHUTDOWN_CHECKPOINT_FLUSH_DEADLINE_MS
} from './renderer-shutdown-checkpoint'
const AWAIT_CHANNEL = 'app:await-before-unload-checkpoint'
describe('registerRendererShutdownCheckpointHandler', () => {
beforeEach(() => {
syncHandlers.clear()
invokeHandlers.clear()
vi.restoreAllMocks()
})
it('stages every shutdown mutation before queueing persistence', () => {
const callOrder: string[] = []
const store = {
stageWorkspaceSessionBeforeUnload: vi.fn((_state, hostId?: string) => {
callOrder.push(`session:${hostId ?? 'local'}`)
}),
updateUI: vi.fn(() => callOrder.push('ui')),
flushPendingOrThrowAsync: vi.fn(() => {
callOrder.push('persist')
return Promise.resolve()
})
}
registerRendererShutdownCheckpointHandler(store as never)
const handler = syncHandlers.get('app:stage-before-unload-sync')
expect(handler).toBeDefined()
const event: { returnValue?: unknown } = {}
const localSession = { activeWorktreeId: 'local-worktree' }
const remoteSession = { activeWorktreeId: 'remote-worktree' }
handler?.(event, {
sessions: [{ state: localSession }, { state: remoteSession, hostId: 'runtime:host-1' }],
ui: { activeView: 'settings' }
})
expect(store.stageWorkspaceSessionBeforeUnload).toHaveBeenNthCalledWith(
1,
localSession,
undefined
)
expect(store.stageWorkspaceSessionBeforeUnload).toHaveBeenNthCalledWith(
2,
remoteSession,
'runtime:host-1'
)
expect(store.updateUI).toHaveBeenCalledWith({ activeView: 'settings' })
expect(store.flushPendingOrThrowAsync).toHaveBeenCalledTimes(1)
// Why: Store fences the staged generation without draining unrelated live mutations.
expect(store.flushPendingOrThrowAsync).toHaveBeenCalledWith(
expect.objectContaining({ drainToStableGeneration: false })
)
expect(callOrder).toEqual(['session:local', 'session:runtime:host-1', 'ui', 'persist'])
expect(event.returnValue).toEqual({ ok: true })
})
it('reports a staging failure so the renderer can retry', () => {
const store = {
stageWorkspaceSessionBeforeUnload: vi.fn(),
updateUI: vi.fn(() => {
throw new Error('disk full')
}),
flushPendingOrThrowAsync: vi.fn(() => Promise.resolve())
}
registerRendererShutdownCheckpointHandler(store as never)
const handler = syncHandlers.get('app:stage-before-unload-sync')
const event: { returnValue?: unknown } = {}
handler?.(event, { sessions: [], ui: { activeView: 'settings' } })
expect(event.returnValue).toEqual({ ok: false })
})
it('does not queue persistence when staging is incomplete', async () => {
const store = {
stageWorkspaceSessionBeforeUnload: vi.fn(),
updateUI: vi.fn(),
flushPendingOrThrowAsync: vi.fn(() => Promise.resolve())
}
registerRendererShutdownCheckpointHandler(store as never)
store.updateUI.mockImplementation(() => {
throw new Error('invalid state')
})
const handler = syncHandlers.get('app:stage-before-unload-sync')
const event: { returnValue?: unknown } = {}
handler?.(event, { sessions: [], ui: { activeView: 'settings' } })
expect(store.flushPendingOrThrowAsync).not.toHaveBeenCalled()
expect(event.returnValue).toEqual({ ok: false })
await expect(invokeHandlers.get(AWAIT_CHANNEL)?.()).resolves.toEqual({ ok: false })
})
it('stages synchronously without waiting on the durable write', () => {
const store = {
stageWorkspaceSessionBeforeUnload: vi.fn(),
updateUI: vi.fn(),
flushPendingOrThrowAsync: vi.fn(() => new Promise<void>(() => {}))
}
registerRendererShutdownCheckpointHandler(store as never)
const handler = syncHandlers.get('app:stage-before-unload-sync')
const event: { returnValue?: unknown } = {}
handler?.(event, { sessions: [], ui: { activeView: 'settings' } })
expect(event.returnValue).toEqual({ ok: true })
})
it('holds the checkpoint open until the durable write settles', async () => {
let resolveFlush!: () => void
const store = {
stageWorkspaceSessionBeforeUnload: vi.fn(),
updateUI: vi.fn(),
flushPendingOrThrowAsync: vi.fn(
() =>
new Promise<void>((next) => {
resolveFlush = next
})
)
}
registerRendererShutdownCheckpointHandler(store as never)
syncHandlers.get('app:stage-before-unload-sync')?.({}, { sessions: [], ui: {} })
const checkpoint = invokeHandlers.get(AWAIT_CHANNEL)?.()
let settled: unknown = 'pending'
void checkpoint?.then((result) => {
settled = result
})
await Promise.resolve()
expect(settled).toBe('pending')
resolveFlush()
await expect(checkpoint).resolves.toEqual({ ok: true })
})
it('reports a failed durable write instead of a successful checkpoint', async () => {
const store = {
stageWorkspaceSessionBeforeUnload: vi.fn(),
updateUI: vi.fn(),
flushPendingOrThrowAsync: vi.fn(() => Promise.reject(new Error('disk full')))
}
vi.spyOn(console, 'error').mockImplementation(() => {})
registerRendererShutdownCheckpointHandler(store as never)
const event: { returnValue?: unknown } = {}
syncHandlers.get('app:stage-before-unload-sync')?.(event, { sessions: [], ui: {} })
expect(event.returnValue).toEqual({ ok: true })
await expect(invokeHandlers.get(AWAIT_CHANNEL)?.()).resolves.toEqual({ ok: false })
})
it('fails the checkpoint when the durable write outlives its deadline', async () => {
const store = {
stageWorkspaceSessionBeforeUnload: vi.fn(),
updateUI: vi.fn(),
flushPendingOrThrowAsync: vi.fn(
(_options: { signal: AbortSignal }) => new Promise<void>(() => {})
)
}
vi.spyOn(console, 'error').mockImplementation(() => {})
vi.useFakeTimers()
try {
registerRendererShutdownCheckpointHandler(store as never)
syncHandlers.get('app:stage-before-unload-sync')?.({}, { sessions: [], ui: {} })
const checkpoint = invokeHandlers.get(AWAIT_CHANNEL)?.()
await vi.advanceTimersByTimeAsync(SHUTDOWN_CHECKPOINT_FLUSH_DEADLINE_MS)
await expect(checkpoint).resolves.toEqual({ ok: false })
expect(store.flushPendingOrThrowAsync.mock.calls[0]?.[0]?.signal.aborted).toBe(true)
} finally {
vi.useRealTimers()
}
})
it('reports success before any checkpoint is staged', async () => {
const store = {
stageWorkspaceSessionBeforeUnload: vi.fn(),
updateUI: vi.fn(),
flushPendingOrThrowAsync: vi.fn(() => Promise.resolve())
}
registerRendererShutdownCheckpointHandler(store as never)
await expect(invokeHandlers.get(AWAIT_CHANNEL)?.()).resolves.toEqual({ ok: true })
})
})