Ordinary database schema upgrades previously ran VACUUM on free pages and showed misleading history-cleanup progress. Separate structure upgrades from required legacy conversions, retaining reclamation intent only for retries within the same startup. Preserve released data without new schema versions or persistent migration markers; Agent and normal writes remain unchanged. Add real SQLite and Worker regressions, bilingual operation-specific startup copy, and migration documentation. Synchronize native Electron drag input with actual resize state without weakening layout assertions. Validation: 47 focused migration tests passed; typecheck, lint and diff checks passed. Final full suite: 5137 passed, 74 skipped, 2 unrelated Electron failures. Both failing Electron tests passed an isolated rerun; full CI remains unverified. Release note: 修复普通数据库结构升级再次整理历史和回收空间的问题;仅在需要转换旧数据时执行相关处理,启动页准确显示当前升级操作。
78 lines
3.7 KiB
TypeScript
78 lines
3.7 KiB
TypeScript
import type { EventEmitter } from 'node:events'
|
|
import type { BrowserWindow, IpcMainInvokeEvent } from 'electron'
|
|
import { beforeEach, expect, it, vi } from 'vitest'
|
|
import { ipcChannels } from '../shared/ipc-channels'
|
|
import { prepareAssistantStorage } from './assistant-storage-startup'
|
|
import { getPendingAssistantStorageUpgrade } from './assistant/assistant-storage-upgrade'
|
|
import { loadMainWindow } from './window'
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
workers: [] as Array<EventEmitter & { options: { workerData: { upgrade: unknown } } }>,
|
|
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('node:fs/promises', () => {
|
|
const stat = vi.fn(async () => ({ size: 1024 }))
|
|
return { stat, default: { stat } }
|
|
})
|
|
vi.mock('./window', () => ({ loadMainWindow: vi.fn() }))
|
|
vi.mock('./assistant/assistant-storage-upgrade', () => ({ getPendingAssistantStorageUpgrade: vi.fn() }))
|
|
vi.mock('node:worker_threads', async () => {
|
|
const { EventEmitter } = await import('node:events')
|
|
class Worker extends EventEmitter {
|
|
constructor(_path: string, readonly options: { workerData: { upgrade: unknown } }) {
|
|
super()
|
|
mocks.workers.push(this)
|
|
}
|
|
}
|
|
return { Worker, default: { Worker } }
|
|
})
|
|
beforeEach(() => {
|
|
mocks.workers.length = 0
|
|
mocks.handlers.clear()
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it.each([false, true])('retains the original reclaimSpace=%s decision across startup retries', async (reclaimSpace) => {
|
|
const upgrade = { migrateNotes: reclaimSpace, reclaimSpace }
|
|
vi.mocked(getPendingAssistantStorageUpgrade).mockReturnValueOnce(upgrade).mockReturnValue(undefined)
|
|
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 preparation = prepareAssistantStorage(window, '/test/assistant.sqlite', new AbortController().signal)
|
|
await vi.waitFor(() => expect(mocks.workers).toHaveLength(1))
|
|
mocks.workers[0]!.emit('message', {
|
|
progress: { stage: 'upgrading', processed: 0, total: 0, bytesBefore: 1024 }
|
|
})
|
|
expect(loadMainWindow).toHaveBeenCalledOnce()
|
|
expect(mocks.handlers.get(ipcChannels.storageUpgradeProgress)!(event)).toMatchObject({ stage: 'upgrading' })
|
|
mocks.workers[0]!.emit('message', { error: 'Retry this startup' })
|
|
mocks.workers[0]!.emit('exit', 0)
|
|
await vi.waitFor(() =>
|
|
expect(mocks.handlers.get(ipcChannels.storageUpgradeProgress)!(event)).toMatchObject({ stage: 'failed' })
|
|
)
|
|
mocks.handlers.get(ipcChannels.storageUpgradeAction)!(event, 'retry')
|
|
await vi.waitFor(() => expect(mocks.workers).toHaveLength(2))
|
|
expect(getPendingAssistantStorageUpgrade).toHaveBeenCalledOnce()
|
|
expect(mocks.workers.map(worker => worker.options.workerData.upgrade)).toEqual([upgrade, upgrade])
|
|
mocks.workers[1]!.emit('message', { done: true })
|
|
mocks.workers[1]!.emit('exit', 0)
|
|
await preparation
|
|
expect(mocks.handlers.size).toBe(0)
|
|
expect(window.setProgressBar).toHaveBeenLastCalledWith(-1)
|
|
})
|
|
|
|
it('does not open an upgrade page or worker for an already-current database', async () => {
|
|
vi.mocked(getPendingAssistantStorageUpgrade).mockReturnValue(undefined)
|
|
await prepareAssistantStorage({} as BrowserWindow, '/test/assistant.sqlite', new AbortController().signal)
|
|
expect(mocks.workers).toHaveLength(0)
|
|
expect(loadMainWindow).not.toHaveBeenCalled()
|
|
})
|