fix: avoid history reclamation during structure-only upgrades
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: 修复普通数据库结构升级再次整理历史和回收空间的问题;仅在需要转换旧数据时执行相关处理,启动页准确显示当前升级操作。
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
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()
|
||||
})
|
||||
Reference in New Issue
Block a user