Main no longer opens desktop business SQLite. A single utility-process
storage owner holds the Assistant, Knowledge, attachment, document-result,
remote-binding and native model-ledger databases and runs whole domain
transactions; Main and Runtime callers use explicit async domain operations.
- Remove the Main readWithFallback path and the supervision worker's
separate business write connection; manifests are scanned read-only and
committed by the sole writer.
- Remote events are ACKed only after commit; process loss reports
unconfirmed writes instead of replaying them.
- Use native utilityProcess messaging with one in-flight bound (32) and
ordered waiting instead of capacity rejection and retries.
- Start the storage process before the window so startup overlaps.
- Keep short tool output in memory and store full output once in owned
storage; paging, branching, restart and deletion reuse that copy.
- Put required runtime launch files under goodbuddy-prefixed owned temp
directories and reclaim confirmed stale ones; stop writing duplicate
parsed.md.
- Add lint guards for SQLite ownership and architecture, concurrency
acceptance fixtures, and maintenance/architecture documentation.
Validation: typecheck and lint pass. Alternating baseline (edf019aa) vs
candidate runs show no regression on single-session, C01, C02, C04 and a
5-minute C06; Main p99 drops from ~21 ms to ~16.7 ms under concurrency.
Fresh portable copy (1.75M events) upgrade content unchanged; real-model
output paging passes with 4 external requests. Full npm test runs had
1-9 different timing-sensitive failures per run that pass when rerun.
Not covered: C03 review publication, C07 source body, app-level C05.
127 lines
5.6 KiB
TypeScript
127 lines
5.6 KiB
TypeScript
import type { BrowserWindow, IpcMainInvokeEvent } from 'electron'
|
|
import { beforeEach, expect, it, vi } from 'vitest'
|
|
import { ipcChannels } from '../shared/ipc-channels'
|
|
import type { AssistantStorageProgress } from '../shared/assistant-storage-contracts'
|
|
import { prepareAssistantStorage } from './assistant-storage-startup'
|
|
import { loadMainWindow } from './window'
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
handlers: new Map<string, (event: unknown, input?: unknown) => unknown>(),
|
|
quit: vi.fn()
|
|
}))
|
|
vi.mock('electron', () => ({
|
|
app: { quit: mocks.quit },
|
|
ipcMain: {
|
|
handle: (channel: string, handler: (event: unknown, input?: unknown) => unknown) => mocks.handlers.set(channel, handler),
|
|
removeHandler: (channel: string) => mocks.handlers.delete(channel)
|
|
}
|
|
}))
|
|
vi.mock('./window', () => ({ loadMainWindow: vi.fn() }))
|
|
beforeEach(() => {
|
|
mocks.handlers.clear()
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
function deferred() {
|
|
let resolve!: () => void
|
|
let reject!: (error: Error) => void
|
|
const promise = new Promise<void>((done, fail) => { resolve = done; reject = fail })
|
|
return { promise, resolve, reject }
|
|
}
|
|
|
|
function fixture() {
|
|
const webContents = { mainFrame: { url: 'app://main' }, getURL: () => 'app://main' }
|
|
const window = { webContents, isDestroyed: () => false, setProgressBar: vi.fn() } as unknown as BrowserWindow
|
|
const event = { sender: webContents, senderFrame: webContents.mainFrame } as unknown as IpcMainInvokeEvent
|
|
const controller = new AbortController()
|
|
const opening = deferred(), retrying = deferred(), closing = deferred()
|
|
const storage = { ready: opening.promise, retry: vi.fn(() => retrying.promise), close: vi.fn(() => closing.promise) }
|
|
let publish!: (progress: AssistantStorageProgress) => void
|
|
const create = vi.fn((listener: typeof publish) => { publish = listener; return storage })
|
|
const preparation = prepareAssistantStorage(window, create, controller.signal)
|
|
return { window, event, controller, opening, retrying, closing, storage, create, publish, preparation }
|
|
}
|
|
|
|
it('publishes utility progress and retries the same client before allowing startup', async () => {
|
|
const f = fixture()
|
|
f.publish({ stage: 'converting', processed: 1, total: 2, bytesBefore: 1024 })
|
|
expect(loadMainWindow).toHaveBeenCalledOnce()
|
|
expect(f.window.setProgressBar).toHaveBeenLastCalledWith(0.5)
|
|
f.opening.reject(new Error('Retry this startup'))
|
|
await vi.waitFor(() => expect(mocks.handlers.get(ipcChannels.storageUpgradeProgress)!(f.event)).toMatchObject({ stage: 'failed' }))
|
|
mocks.handlers.get(ipcChannels.storageUpgradeAction)!(f.event, 'retry')
|
|
await vi.waitFor(() => expect(f.storage.retry).toHaveBeenCalledOnce())
|
|
const completed = vi.fn()
|
|
void f.preparation.then(completed)
|
|
await Promise.resolve()
|
|
expect(completed).not.toHaveBeenCalled()
|
|
expect(f.create).toHaveBeenCalledOnce()
|
|
f.retrying.resolve()
|
|
await f.preparation
|
|
expect(mocks.handlers.size).toBe(0)
|
|
expect(f.window.setProgressBar).toHaveBeenLastCalledWith(-1)
|
|
expect(f.storage.close).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('keeps the client identity and waits for replacement readiness across repeated process failures', async () => {
|
|
const f = fixture()
|
|
f.opening.reject(new Error('Initial utility exited'))
|
|
await vi.waitFor(() => expect(mocks.handlers.get(ipcChannels.storageUpgradeProgress)!(f.event)).toMatchObject({ stage: 'failed' }))
|
|
mocks.handlers.get(ipcChannels.storageUpgradeAction)!(f.event, 'retry')
|
|
await vi.waitFor(() => expect(f.storage.retry).toHaveBeenCalledTimes(1))
|
|
f.retrying.reject(new Error('Replacement utility exited'))
|
|
await vi.waitFor(() => expect(mocks.handlers.get(ipcChannels.storageUpgradeProgress)!(f.event)).toMatchObject({ error: 'Replacement utility exited' }))
|
|
const replacement = deferred()
|
|
f.storage.retry.mockReturnValue(replacement.promise)
|
|
mocks.handlers.get(ipcChannels.storageUpgradeAction)!(f.event, 'retry')
|
|
await vi.waitFor(() => expect(f.storage.retry).toHaveBeenCalledTimes(2))
|
|
expect(f.create).toHaveBeenCalledTimes(1)
|
|
replacement.resolve()
|
|
await f.preparation
|
|
expect(f.storage.close).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not open an upgrade page when storage becomes ready without an upgrade', async () => {
|
|
const f = fixture()
|
|
f.opening.resolve()
|
|
await f.preparation
|
|
expect(loadMainWindow).not.toHaveBeenCalled()
|
|
expect(mocks.handlers.size).toBe(0)
|
|
})
|
|
|
|
it.each([false, true])('awaits utility close on quit while failed=%s', async failed => {
|
|
const f = fixture()
|
|
if (failed) {
|
|
f.opening.reject(new Error('Startup failed'))
|
|
await vi.waitFor(() => expect(loadMainWindow).toHaveBeenCalledOnce())
|
|
}
|
|
f.controller.abort()
|
|
await vi.waitFor(() => expect(f.storage.close).toHaveBeenCalledOnce())
|
|
const settled = vi.fn()
|
|
void f.preparation.then(settled)
|
|
await Promise.resolve()
|
|
expect(settled).not.toHaveBeenCalled()
|
|
f.closing.resolve()
|
|
await f.preparation
|
|
expect(f.storage.retry).not.toHaveBeenCalled()
|
|
expect(mocks.handlers.size).toBe(0)
|
|
})
|
|
|
|
it('rejects upgrade actions from an unrelated sender', async () => {
|
|
const f = fixture()
|
|
expect(() => mocks.handlers.get(ipcChannels.storageUpgradeAction)!({ sender: {} }, 'retry')).toThrow()
|
|
f.opening.resolve()
|
|
await f.preparation
|
|
})
|
|
|
|
it('closes the owner when readiness fails at the same time as quit and reports an unconfirmed close', async () => {
|
|
const f = fixture()
|
|
f.opening.reject(new Error('Storage exited'))
|
|
f.controller.abort()
|
|
await vi.waitFor(() => expect(f.storage.close).toHaveBeenCalledOnce())
|
|
const result = expect(f.preparation).rejects.toThrow('Drain unconfirmed')
|
|
f.closing.reject(new Error('Drain unconfirmed'))
|
|
await result
|
|
expect(mocks.handlers.size).toBe(0)
|
|
})
|