Files
goodbuddy/tests/assistant-storage-worker.test.ts
T
mesalogo 4c7193e1ea 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: 修复普通数据库结构升级再次整理历史和回收空间的问题;仅在需要转换旧数据时执行相关处理,启动页准确显示当前升级操作。
2026-09-26 12:42:08 +08:00

97 lines
4.7 KiB
TypeScript

// @vitest-environment node
import { randomUUID } from 'node:crypto'
import { mkdtemp, rm, stat } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { DatabaseSync } from 'node:sqlite'
import { Worker } from 'node:worker_threads'
import { build } from 'esbuild'
import { afterAll, beforeAll, expect, it } from 'vitest'
import { AssistantDatabase, ASSISTANT_DATABASE_SCHEMA_VERSION } from '../src/main/assistant/assistant-database'
import { getPendingAssistantStorageUpgrade } from '../src/main/assistant/assistant-storage-upgrade'
import { assistantStorageProgressSchema } from '../src/shared/assistant-storage-contracts'
let directory: string
let workerPath: string
beforeAll(async () => {
directory = await mkdtemp(join(tmpdir(), 'goodbuddy-storage-worker-'))
workerPath = join(directory, 'worker.cjs')
await build({
entryPoints: [resolve('src/main/assistant-storage-worker.ts')],
outfile: workerPath, bundle: true, platform: 'node', format: 'cjs', target: 'node24'
})
})
afterAll(async () => {
if (directory) await rm(directory, { recursive: true, force: true })
})
it.each([false, true])('runs the real startup worker with legacy notes=%s without touching unrelated data', async (legacyNotes) => {
const root = await mkdtemp(join(directory, 'case-'))
const databasePath = join(root, 'assistant.sqlite')
const database = new AssistantDatabase(databasePath)
database.initialize(root)
const note = database.createMagicNote({
title: 'Worker note', content: { version: 1, ops: [{ insert: 'Keep note\n' }] }
})
const header = { id: randomUUID(), title: 'Worker chat', updatedAt: 1000 }
const message = { id: randomUUID(), role: 'assistant' as const, state: 'complete' as const,
content: 'Normal chat'.repeat(100_000), createdAt: 1000 }
database.saveLocalConversations([{ header, messages: [message] }])
database.saveLocalConversations([{ header, messages: [{ ...message, content: 'Keep response' }] }])
const expected = database.getConversation(header.id)
database.close()
const sql = new DatabaseSync(databasePath)
try {
sql.exec(`DROP VIEW supervision_review_current;
DROP TABLE supervision_review_navigation; DROP TABLE supervision_review_batches;
DROP TABLE supervision_review_sources; DROP TABLE supervision_review_runs;
PRAGMA user_version = 46;`)
if (legacyNotes) sql.prepare('UPDATE magic_note_entries SET content_json = ? WHERE id = ?')
.run(JSON.stringify(note.entries[0]!.content), note.entries[0]!.id)
sql.exec('PRAGMA wal_checkpoint(TRUNCATE)')
expect(sql.prepare('PRAGMA freelist_count').get()!.freelist_count).toBeGreaterThan(0)
const bytesBefore = (await stat(databasePath)).size
const upgrade = getPendingAssistantStorageUpgrade(databasePath)!
expect(upgrade).toEqual({ migrateNotes: legacyNotes, reclaimSpace: legacyNotes })
const run = async (cancelled: boolean) => {
const cancellation = new SharedArrayBuffer(4)
if (cancelled) Atomics.store(new Int32Array(cancellation), 0, 1)
const worker = new Worker(workerPath, { workerData: { databasePath, cancellation, upgrade } })
const messages: Array<{ progress?: unknown; done?: boolean; cancelled?: boolean; error?: string }> = []
worker.on('message', message => messages.push(message))
try {
const code = await new Promise<number>((resolve, reject) => {
worker.once('error', reject)
worker.once('exit', resolve)
})
expect(code).toBe(0)
return messages
} finally { await worker.terminate() }
}
expect((await run(true)).at(-1)).toMatchObject({ cancelled: true })
expect(sql.prepare('PRAGMA user_version').get()!.user_version).toBe(46)
const messages = await run(false)
expect(messages.at(-1)).toEqual({ done: true })
expect(messages.some(message => message.error)).toBe(false)
const stages = messages.flatMap(message => message.progress
? [assistantStorageProgressSchema.parse(message.progress).stage] : [])
expect(stages).toContain('upgrading')
if (legacyNotes) {
expect(stages).toContain('converting')
expect(stages).toContain('compacting')
} else {
expect(stages).toEqual(['upgrading'])
expect((await stat(databasePath)).size).toBeGreaterThanOrEqual(bytesBefore)
}
expect(sql.prepare('PRAGMA user_version').get()!.user_version).toBe(ASSISTANT_DATABASE_SCHEMA_VERSION)
expect(getPendingAssistantStorageUpgrade(databasePath)).toBeUndefined()
database.initialize(root)
expect(database.getConversation(header.id)).toEqual(expected)
expect(database.getMagicNote(note.id)).toEqual(note)
expect(sql.prepare('PRAGMA integrity_check').get()).toEqual({ integrity_check: 'ok' })
} finally {
database.close()
sql.close()
}
}, 30_000)