Files
goodbuddy/src/main/magic-notes/magic-note-storage.test.ts
T
mesalogo 13ff182858 fix: reduce redundant work during chat and note startup
Avoid hidden activity renders, no-op message saves and attachment scans when pinning conversations. Reuse note body inspection and skip unchanged attachment reference writes while preserving recovery and cleanup behavior.

Skip unnecessary Markdown normalization and memoize subagent lookups without delaying live rich text. Add storage operation counts and real Electron rendering equivalence benchmarks; full-render speedup remains unproven.

Validation: 5397 tests passed, 84 skipped; typecheck passed. Lint passed excluding two existing untracked sidebar probe files.

Release note: Reduce repeated rendering and disk work during chat, conversation pinning and note startup while preserving existing content and interactions.
2026-09-28 20:53:43 +08:00

524 lines
31 KiB
TypeScript

import { randomUUID } from 'node:crypto'
import fs, { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { backup, DatabaseSync } from 'node:sqlite'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { MagicNoteCanvasContent, MagicNoteContent } from '../../shared/magic-notes-contracts'
import { AssistantDatabase, ASSISTANT_DATABASE_SCHEMA_VERSION } from '../assistant/assistant-database'
import { getPendingAssistantStorageUpgrade, upgradeAssistantStorage } from '../assistant/assistant-storage-upgrade'
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs')>()
const readFileSync = vi.fn(actual.readFileSync)
return { ...actual, readFileSync, default: { ...actual, readFileSync } }
})
const actualFs = { renameSync: fs.renameSync, rmSync: fs.rmSync }
const directories: string[] = []
const connections: Array<{ close(): void }> = []
afterEach(() => {
vi.restoreAllMocks()
for (const connection of connections.splice(0)) connection.close()
for (const directory of directories.splice(0)) rmSync(directory, { recursive: true, force: true })
vi.mocked(readFileSync).mockClear()
})
function setup(options: ConstructorParameters<typeof AssistantDatabase>[1] = {}) {
const directory = mkdtempSync(join(tmpdir(), 'goodbuddy-note-files-'))
directories.push(directory)
const path = join(directory, 'assistant.sqlite')
const database = new AssistantDatabase(path, options)
connections.push(database)
database.initialize(directory)
const sql = new DatabaseSync(path)
connections.push(sql)
return { database, sql, directory, path }
}
const png = 'data:image/png;base64,iVBORw0KGgo='
const rich: MagicNoteContent = { version: 1, ops: [
{ insert: 'Task' }, { insert: '\n', attributes: { list: 'unchecked' } },
{ insert: { image: png } },
{ insert: { localVideo: { name: 'clip.mp4', mimeType: 'video/mp4', size: 12, dataUrl: `data:video/mp4;base64,${Buffer.from('0000ftyp0000').toString('base64')}` } } },
{ insert: { attachment: { name: 'file.txt', mimeType: 'text/plain', size: 5, dataUrl: 'data:text/plain;base64,aGVsbG8=' } } },
{ insert: '\n' }
] }
function canvas(size = 40): MagicNoteCanvasContent {
return {
version: 2, kind: 'paged-canvas',
pages: [{ id: 'page', width: 794, height: 1123,
background: { type: 'pdf', assetId: 'original-pdf-id', pageNumber: 1, text: 'PDF text' },
objects: [{ type: 'Image', src: png }, { type: 'IText', text: 'Annotation', left: 0 }] }],
flow: { ops: [{ insert: 'Canvas task' }, { insert: '\n', attributes: { list: 'unchecked' } }] },
assets: [{ id: 'original-pdf-id', name: 'document.pdf', mimeType: 'application/pdf',
dataUrl: `data:application/pdf;base64,${Buffer.from(`%PDF-${'x'.repeat(size)}`).toString('base64')}` }],
preview: png
}
}
describe('Magic note SQLite and filesystem storage', () => {
it('measures current entry startup body reads on real SQLite and filesystem fixtures', () => {
const { database, directory, sql } = setup()
const paths: string[] = []
for (let index = 0; index < 120; index++) {
const content = index % 2 ? canvas() : structuredClone(rich)
if (content.version === 1) content.ops.unshift({ insert: 'x'.repeat(32 * 1024) })
else content.pages[0]!.objects.push({ type: 'IText', text: 'x'.repeat(32 * 1024) })
const note = database.createMagicNote({ title: `Synthetic ${index}`, content })
paths.push(join(directory, 'notes', note.id, 'entries', `${note.entries[0]!.id}.json`))
}
database.close()
const bodyPaths = new Set(paths)
const reads = vi.mocked(readFileSync)
const durations: number[] = []
const counts: number[] = []
for (let run = 0; run < 5; run++) {
reads.mockClear()
const start = performance.now()
database.initialize(directory)
durations.push(Number((performance.now() - start).toFixed(2)))
counts.push(reads.mock.calls.filter(([path]) => bodyPaths.has(String(path))).length)
for (const path of paths) expect(reads.mock.calls.filter(([readPath]) => String(readPath) === path)).toHaveLength(1)
expect(reads.mock.calls.some(([path]) => String(path).includes(join(directory, 'notes')) && String(path).includes('assets'))).toBe(false)
database.close()
}
console.info('Synthetic note startup', { entries: paths.length, bodyBytes: paths.reduce((sum, path) => sum + statSync(path).size, 0), counts, durationsMs: durations })
expect(counts).toEqual(Array(5).fill(paths.length))
expect(sql.prepare('PRAGMA integrity_check').get()).toEqual({ integrity_check: 'ok' })
})
it('upgrades schema 47 with nullable sources without rewriting bodies and reopens idempotently', () => {
const { database, sql, path, directory } = setup()
const created = database.createMagicNote({ title: 'Existing note', content: rich })
const before = database.getMagicNote(created.id)
const bodyPath = join(directory, 'notes', created.id, 'entries', `${created.createdEntryId}.json`)
const body = readFileSync(bodyPath)
const modifiedAt = statSync(bodyPath).mtimeMs
database.close()
// Reconstruct the previous released schema; production never downgrades databases.
sql.exec('ALTER TABLE magic_note_entries DROP COLUMN source_json; PRAGMA user_version = 47;')
upgradeAssistantStorage(path, () => undefined)
expect(sql.prepare('PRAGMA user_version').get()).toEqual({ user_version: ASSISTANT_DATABASE_SCHEMA_VERSION })
expect(sql.prepare('SELECT source_json FROM magic_note_entries').get()).toEqual({ source_json: null })
expect(sql.prepare('PRAGMA integrity_check').get()).toEqual({ integrity_check: 'ok' })
database.initialize(directory)
expect(database.getMagicNote(created.id)).toEqual(before)
expect(database.getMagicNote(created.id).entries[0]).not.toHaveProperty('source')
expect(readFileSync(bodyPath)).toEqual(body)
expect(statSync(bodyPath).mtimeMs).toBe(modifiedAt)
database.close()
database.initialize(directory)
expect(database.getMagicNote(created.id)).toEqual(before)
})
it.each(['missing', 'invalid-json', 'invalid-media'] as const)('retains all assets when a later retained body has %s corruption', (failure) => {
const { database, directory } = setup()
const note = database.createMagicNote({ title: 'Retained bodies', content: rich })
const entries = database.createMagicNoteEntry({ noteId: note.id, content: canvas(), plainText: '' }).entries
const root = join(directory, 'notes', note.id)
const bodyPath = join(root, 'entries', `${entries[1]!.id}.json`)
const original = readFileSync(bodyPath, 'utf8')
const manifest = readFileSync(join(root, 'note.json'), 'utf8')
writeFileSync(join(root, 'assets', 'unused.bin'), 'keep until inspection succeeds')
const assets = readdirSync(join(root, 'assets')).map((name) => [name, readFileSync(join(root, 'assets', name))] as const)
database.close()
if (failure === 'missing') rmSync(bodyPath)
else if (failure === 'invalid-json') writeFileSync(bodyPath, '{')
else {
const body = JSON.parse(original)
body.content.pages = null
writeFileSync(bodyPath, JSON.stringify(body))
}
const warning = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
if (failure === 'invalid-media') {
database.initialize(directory)
expect(warning).toHaveBeenCalledWith('Magic note file cleanup deferred', expect.objectContaining({ noteId: note.id }))
} else expect(() => database.initialize(directory)).toThrow()
for (const [name, bytes] of assets) expect(readFileSync(join(root, 'assets', name))).toEqual(bytes)
expect(readFileSync(join(root, 'note.json'), 'utf8')).toBe(manifest)
database.close()
writeFileSync(bodyPath, original)
vi.mocked(readFileSync).mockClear()
database.initialize(directory)
for (const entry of entries) {
const path = join(root, 'entries', `${entry.id}.json`)
expect(vi.mocked(readFileSync).mock.calls.filter(([readPath]) => String(readPath) === path)).toHaveLength(1)
}
expect(database.getMagicNote(note.id).entries).toEqual(entries)
expect(existsSync(join(root, 'assets', 'unused.bin'))).toBe(false)
})
it('preserves textual data URLs and arbitrary $asset objects through save, migration and reopen', () => {
const { database, sql, path, directory } = setup()
const text = 'data:text/plain;base64,aGVsbG8'
const custom = { $asset: 'not-a-storage-reference', mimeType: 'text/plain', name: text, text }
const richText: MagicNoteContent = { version: 1, ops: [{ insert: text }, { insert: png }, { insert: '\n' }] }
const drawing = canvas()
drawing.pages[0]!.objects.push({ type: 'IText', ...custom }, { type: 'Group', objects: [{ type: 'Image', src: png, name: text, custom }] })
drawing.flow!.ops.push({ insert: text }, { insert: { image: png } }, { insert: { custom } })
const notes = [richText, drawing].map((content) => database.getMagicNote(database.createMagicNote({ title: 'Literal URLs', content }).id))
for (const [index, note] of notes.entries()) expect(note.entries[0]!.content).toEqual([richText, drawing][index])
const disk = JSON.parse(readFileSync(join(directory, 'notes', notes[1]!.id, 'entries', `${notes[1]!.entries[0]!.id}.json`), 'utf8'))
expect(disk.content.pages[0].objects[2]).toEqual({ type: 'IText', ...custom })
expect(disk.content.flow.ops.at(-2).insert.image).toHaveProperty('$asset')
expect(existsSync(join(directory, 'notes', notes[0]!.id, 'assets'))).toBe(false)
database.close()
for (const note of notes) sql.prepare('UPDATE magic_note_entries SET content_json = ? WHERE id = ?').run(JSON.stringify(note.entries[0]!.content), note.entries[0]!.id)
rmSync(join(directory, 'notes'), { recursive: true })
upgradeAssistantStorage(path, () => undefined)
database.initialize(directory)
expect(notes.map((note) => database.getMagicNote(note.id))).toEqual(notes)
})
it.each(['manifest', 'gc'] as const)('returns successful creates/updates despite deferred %s failures and retries cleanup', (failure) => {
const changed = vi.fn()
const logged = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const { database, directory } = setup({ onMagicNotesChanged: changed })
let failing = true
vi.spyOn(fs, 'renameSync').mockImplementation((source, target) => {
if (failing && failure === 'manifest' && String(target).endsWith('note.json')) throw new Error('manifest rename blocked')
actualFs.renameSync(source, target)
})
vi.spyOn(fs, 'rmSync').mockImplementation((path, options) => {
if (failing && failure === 'gc' && String(path).endsWith('unused.bin')) throw new Error('asset removal blocked')
actualFs.rmSync(path, options)
})
const note = database.createMagicNote({ title: 'Committed', content: rich })
const root = join(directory, 'notes', note.id)
writeFileSync(join(root, 'assets', 'unused.bin'), 'unused')
const appended = database.createMagicNoteEntry({ noteId: note.id, content: rich, plainText: '' })
const first = appended.entries[0]!
const saved = database.updateMagicNoteEntry({ entryId: first.id, expectedRevision: first.revision, content: canvas(), plainText: '' })
expect(saved.entries).toHaveLength(2)
expect(saved.entries[0]!.content).toEqual(canvas())
expect(changed).toHaveBeenCalledTimes(3)
expect(logged).toHaveBeenCalledWith('Magic note file cleanup deferred', expect.objectContaining({ noteId: note.id, error: expect.any(Error) }))
database.close()
database.initialize(directory)
expect(database.getMagicNote(note.id)).toEqual(saved)
failing = false
expect(database.listMagicNotes()[0]!.entryCount).toBe(2)
expect(existsSync(join(root, 'assets', 'unused.bin'))).toBe(false)
expect(JSON.parse(readFileSync(join(root, 'note.json'), 'utf8')).entries).toEqual(saved.entries.map((entry) => entry.id))
})
it.each(['delete', 'reset'] as const)('notifies and completes %s after SQL commit when filesystem removal fails', (operation) => {
const changed = vi.fn()
const logged = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const { database, directory } = setup({ onMagicNotesChanged: changed })
const note = database.createMagicNote({ title: 'Remove', content: rich })
const root = join(directory, 'notes', note.id)
let failing = true
vi.spyOn(fs, 'rmSync').mockImplementation((path, options) => {
if (failing && (String(path) === root || String(path) === join(directory, 'notes'))) throw new Error('directory removal blocked')
actualFs.rmSync(path, options)
})
if (operation === 'delete') database.deleteMagicNote(note.id)
else database.clearAssistantData()
expect(changed).toHaveBeenCalledTimes(2)
expect(database.listMagicNotes()).toEqual([])
expect(existsSync(root)).toBe(true)
expect(logged).toHaveBeenCalled()
// A delayed reset cleanup must not remove notes created afterwards.
const retained = database.getMagicNote(database.createMagicNote({ title: 'New note', content: canvas() }).id)
failing = false
database.listMagicNotes()
expect(existsSync(root)).toBe(false)
expect(database.getMagicNote(retained.id)).toEqual(retained)
})
it('still rejects authoritative body rename failures before committing SQL', () => {
const { database } = setup()
const note = database.getMagicNote(database.createMagicNote({ title: 'Body error', content: rich }).id)
const entry = note.entries[0]!
vi.spyOn(fs, 'renameSync').mockImplementation((source, target) => {
if (String(target).endsWith(`${entry.id}.json`)) throw new Error('body rename blocked')
actualFs.renameSync(source, target)
})
expect(() => database.updateMagicNoteEntry({ entryId: entry.id, content: canvas(), expectedRevision: entry.revision, plainText: '' })).toThrow('body rename blocked')
expect(database.getMagicNote(note.id)).toEqual(note)
})
it.each(['cancel', 'vacuum'] as const)('retries free-page reclamation after %s interrupts a fully converted migration', (failure) => {
const { database, sql, path } = setup()
const note = database.createMagicNote({ title: 'Migration retry', content: canvas(1024 * 1024) })
database.close()
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)')
const confirmedUpgrade = getPendingAssistantStorageUpgrade(path)!
let cancelled = false
const exec = DatabaseSync.prototype.exec
const injected = vi.spyOn(DatabaseSync.prototype, 'exec').mockImplementation(function (this: DatabaseSync, statement) {
if (failure === 'vacuum' && statement.includes('VACUUM')) throw new Error('vacuum disk full')
return exec.call(this, statement)
})
expect(() => upgradeAssistantStorage(path, (progress) => {
if (failure === 'cancel' && progress.stage === 'converting' && progress.processed === progress.total) cancelled = true
}, () => cancelled)).toThrow(failure === 'cancel' ? 'cancelled' : 'vacuum disk full')
injected.mockRestore()
expect(sql.prepare("SELECT json_extract(content_json, '$.storage') AS storage FROM magic_note_entries").get()!.storage).toBe('file')
expect(sql.prepare('PRAGMA freelist_count').get()!.freelist_count).toBeGreaterThan(0)
expect(getPendingAssistantStorageUpgrade(path)).toBeUndefined()
const stages: string[] = []
upgradeAssistantStorage(path, (progress) => stages.push(progress.stage), () => false, {
confirmedUpgrade
})
expect(stages).toContain('compacting')
expect(sql.prepare('PRAGMA freelist_count').get()!.freelist_count).toBe(0)
})
it('hydrates rich media, searches, writes todos back, shares assets, deletes and resets', () => {
const { database, sql, directory } = setup()
const note = database.createMagicNote({ title: 'Media', content: rich })
const first = note.entries[0]!
const root = join(directory, 'notes', note.id)
expect(first.content).toEqual(rich)
expect(readdirSync(join(root, 'assets'))).toHaveLength(3)
const body = readFileSync(join(root, 'entries', `${first.id}.json`), 'utf8')
expect(body).not.toContain('base64')
expect(body).toContain('$asset')
expect(sql.prepare('SELECT content_json FROM magic_note_entries WHERE id = ?').get(first.id)).toEqual({
content_json: JSON.stringify({ storage: 'file', version: 1, revision: 0 })
})
const second = database.createMagicNoteEntry({ noteId: note.id, content: rich, plainText: 'ignored' }).entries[1]!
expect(readdirSync(join(root, 'assets'))).toHaveLength(3)
const todo = database.listMagicTodos().find((item) => item.entryId === first.id)!
database.updateMagicTodo({ todoId: todo.id, expectedRevision: todo.revision, completed: true })
expect(database.getMagicNoteEntry(first.id).content).toMatchObject({ ops: expect.arrayContaining([{ insert: '\n', attributes: { list: 'checked' } }]) })
expect(database.searchMagicNotes('Task', 20)).toHaveLength(2)
database.deleteMagicNoteEntry(first.id)
expect(existsSync(join(root, 'entries', `${first.id}.json`))).toBe(false)
expect(database.getMagicNoteEntry(second.id).content).toEqual(rich)
expect(readdirSync(join(root, 'assets'))).toHaveLength(3)
expect(JSON.parse(readFileSync(join(root, 'note.json'), 'utf8')).entries).toEqual([second.id])
database.deleteMagicNote(note.id)
expect(existsSync(root)).toBe(false)
expect(database.listMagicTodos()).toEqual([])
database.createMagicNote({ title: 'Reset', content: canvas() })
database.clearAssistantData()
expect(existsSync(join(directory, 'notes'))).toBe(false)
expect(database.listMagicNotes()).toEqual([])
})
it('migrates both formats via the startup worker path and preserves metadata, comments and todo IDs', () => {
const { database, sql, path, directory } = setup()
const original = [rich, canvas()].map((content) => database.createMagicNote({ title: 'Legacy', content }))
for (const note of original) {
const entry = note.entries[0]!
database.saveMagicNoteAnalysis({ entryId: entry.id, expectedRevision: entry.revision,
comments: [{ id: randomUUID(), kind: 'summary', content: 'Keep comment' }] })
}
const before = original.map((note) => database.getMagicNote(note.id))
const todos = database.listMagicTodos()
database.close()
for (const note of before) {
const entry = note.entries[0]!
sql.prepare('UPDATE magic_note_entries SET content_json = ? WHERE id = ?').run(JSON.stringify(entry.content), entry.id)
}
sql.exec(`ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json;
DROP TABLE activity_history_records;
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
DROP VIEW IF EXISTS supervision_review_current;
DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
ALTER TABLE magic_note_entries DROP COLUMN source_json;
PRAGMA user_version = 37`)
rmSync(join(directory, 'notes'), { recursive: true })
const progress: number[] = []
upgradeAssistantStorage(path, (value) => progress.push(value.processed))
expect(progress).toContain(2)
expect(sql.prepare('PRAGMA user_version').get()).toEqual({ user_version: ASSISTANT_DATABASE_SCHEMA_VERSION })
database.initialize(directory)
expect(original.map((note) => database.getMagicNote(note.id))).toEqual(before)
expect(database.listMagicTodos()).toEqual(todos)
database.close()
database.initialize(directory)
expect(original.map((note) => database.getMagicNote(note.id))).toEqual(before)
expect(sql.prepare('SELECT SUM(length(content_json)) AS bytes FROM magic_note_entries').get()!.bytes).toBeLessThan(200)
expect(sql.prepare('PRAGMA integrity_check').get()).toEqual({ integrity_check: 'ok' })
})
it('retains legacy payload on a failed migration and retries partially completed conversion', () => {
const { database, sql, directory, path } = setup()
const note = database.createMagicNote({ title: 'Legacy', content: rich })
database.createMagicNoteEntry({ noteId: note.id, content: canvas(), plainText: '' })
const entries = database.getMagicNote(note.id).entries
database.close()
for (const entry of entries) sql.prepare('UPDATE magic_note_entries SET content_json = ? WHERE id = ?').run(JSON.stringify(entry.content), entry.id)
rmSync(join(directory, 'notes'), { recursive: true })
sql.exec(`CREATE TRIGGER fail_note_migration BEFORE UPDATE OF content_json ON magic_note_entries
BEGIN SELECT RAISE(ABORT, 'migration index failure'); END`)
expect(() => upgradeAssistantStorage(path, () => undefined)).toThrow('migration index failure')
expect(JSON.parse(String(sql.prepare('SELECT content_json FROM magic_note_entries WHERE id = ?').get(entries[0]!.id)!.content_json))).toEqual(rich)
sql.exec('DROP TRIGGER fail_note_migration')
let cancelled = false
expect(() => upgradeAssistantStorage(path, (progress) => {
if (progress.stage === 'converting') cancelled = true
}, () => cancelled)).toThrow('cancelled')
expect(sql.prepare("SELECT COUNT(*) AS count FROM magic_note_entries WHERE json_extract(content_json, '$.storage') = 'file'").get()!.count).toBe(1)
upgradeAssistantStorage(path, () => undefined)
database.initialize(directory)
expect(database.getMagicNote(note.id).entries).toEqual(entries)
})
it('reclaims migrated SQLite payload space and restores a coordinated database/files backup', async () => {
const { database, sql, directory, path } = setup()
const note = database.getMagicNote(database.createMagicNote({ title: 'Large legacy PDF', content: canvas(2 * 1024 * 1024) }).id)
const entry = note.entries[0]!
database.close()
sql.prepare('UPDATE magic_note_entries SET content_json = ? WHERE id = ?').run(JSON.stringify(entry.content), entry.id)
sql.exec('PRAGMA wal_checkpoint(TRUNCATE)')
const bytesBefore = statSync(path).size
const stages: string[] = []
upgradeAssistantStorage(path, (progress) => stages.push(progress.stage))
expect(stages).toContain('compacting')
expect(bytesBefore - statSync(path).size).toBeGreaterThan(2 * 1024 * 1024)
database.initialize(directory)
expect(database.getMagicNote(note.id)).toEqual(note)
const destination = join(directory, 'backup')
mkdirSync(destination)
await backup(sql, join(destination, 'assistant.sqlite'))
cpSync(join(directory, 'notes'), join(destination, 'notes'), { recursive: true })
database.deleteMagicNote(note.id)
const restored = new AssistantDatabase(join(destination, 'assistant.sqlite'))
connections.push(restored)
restored.initialize(destination)
expect(restored.getMagicNote(note.id)).toEqual(note)
expect(restored.listMagicTodos()[0]).toMatchObject({ title: 'Canvas task' })
})
it('rejects stale entry and todo revisions before touching files', () => {
const { database, directory } = setup()
const note = database.createMagicNote({ title: 'Conflict', content: rich })
const entry = note.entries[0]!
const path = join(directory, 'notes', note.id, 'entries', `${entry.id}.json`)
const before = readFileSync(path)
const mtime = statSync(path).mtimeMs
database.saveMagicNoteAnalysis({ entryId: entry.id, expectedRevision: 0, comments: [] })
expect(() => database.updateMagicNoteEntry({ entryId: entry.id, expectedRevision: 0, content: canvas(), plainText: '' })).toThrow('已被更新')
const todo = database.listMagicTodos()[0]!
expect(() => database.updateMagicTodo({ todoId: todo.id, expectedRevision: 100, completed: true })).toThrow('已被更新')
expect(readFileSync(path)).toEqual(before)
expect(statSync(path).mtimeMs).toBe(mtime)
expect(readdirSync(join(directory, 'notes', note.id, 'assets'))).toHaveLength(3)
})
it.each([false, true])('repairs a committed file after a real SQLite index failure (reopen=%s)', (reopen) => {
const { database, sql, directory } = setup()
const note = database.createMagicNote({ title: 'Repair', content: rich })
const entry = note.entries[0]!
database.saveMagicNoteAnalysis({ entryId: entry.id, expectedRevision: entry.revision,
comments: [{ id: randomUUID(), kind: 'summary', content: 'Stale analysis' }] })
const analyzed = database.getMagicNoteEntry(entry.id)
sql.exec(`CREATE TRIGGER fail_note_update BEFORE UPDATE OF content_json ON magic_note_entries
BEGIN SELECT RAISE(ABORT, 'index unavailable'); END`)
expect(() => database.updateMagicNoteEntry({ entryId: entry.id, expectedRevision: analyzed.revision, content: canvas(), plainText: '' })).toThrow('index unavailable')
expect(() => database.searchMagicNotes('PDF text', 10)).toThrow('index unavailable')
sql.exec('DROP TRIGGER fail_note_update')
if (reopen) { database.close(); database.initialize(directory) }
expect(database.searchMagicNotes('PDF text', 10)).toHaveLength(1)
expect(database.getMagicNoteEntry(entry.id)).toMatchObject({ content: canvas(), revision: analyzed.revision + 1, comments: [], analyzedAt: undefined })
const assetNames = readdirSync(join(directory, 'notes', note.id, 'assets'))
expect(assetNames).toHaveLength(2)
expect(assetNames.some((name) => name.endsWith('.pdf'))).toBe(true)
expect(database.listMagicTodos()).toEqual([expect.objectContaining({ title: 'Canvas task' })])
expect(() => database.updateMagicNoteEntry({ entryId: entry.id, expectedRevision: 0, content: rich, plainText: '' })).toThrow('已被更新')
})
it('repairs todo writeback from disk without replacing the todo identity', () => {
const { database, sql } = setup()
database.createMagicNote({ title: 'Todo repair', content: canvas() })
const todo = database.listMagicTodos()[0]!
sql.exec(`CREATE TRIGGER fail_todo_update BEFORE UPDATE OF completed ON magic_todos
BEGIN SELECT RAISE(ABORT, 'todo index unavailable'); END`)
expect(() => database.updateMagicTodo({ todoId: todo.id, expectedRevision: todo.revision, completed: true })).toThrow('todo index unavailable')
sql.exec('DROP TRIGGER fail_todo_update')
expect(database.getMagicTodo(todo.id)).toMatchObject({ id: todo.id, completed: true, revision: todo.revision + 1 })
expect(database.getMagicNoteEntry(todo.entryId).content).toMatchObject({ flow: { ops: expect.arrayContaining([{ insert: '\n', attributes: { list: 'checked' } }]) } })
})
it('rolls back failed creates and cleans their unaccepted files without affecting existing notes', () => {
const { database, sql, directory } = setup()
const retained = database.getMagicNote(database.createMagicNote({ title: 'Keep', content: rich }).id)
sql.exec(`CREATE TRIGGER fail_note_insert BEFORE INSERT ON magic_note_entries
BEGIN SELECT RAISE(ABORT, 'insert unavailable'); END`)
expect(() => database.createMagicNote({ title: 'Failed', content: canvas() })).toThrow('insert unavailable')
expect(() => database.createMagicNoteEntry({ noteId: retained.id, content: canvas(), plainText: '' })).toThrow('insert unavailable')
sql.exec('DROP TRIGGER fail_note_insert')
expect(database.listMagicNotes()).toHaveLength(1)
expect(database.getMagicNote(retained.id)).toEqual(retained)
expect(readdirSync(join(directory, 'notes'))).toEqual([retained.id])
expect(readdirSync(join(directory, 'notes', retained.id, 'assets'))).toHaveLength(3)
})
it('keeps body unchanged if binary persistence fails and does not hide missing body errors', () => {
const { database, directory } = setup()
const note = database.createMagicNote({ title: 'Failure', content: { version: 1, ops: [{ insert: 'Plain\n' }] } })
const entry = note.entries[0]!
const root = join(directory, 'notes', note.id)
const path = join(root, 'entries', `${entry.id}.json`)
const before = readFileSync(path)
writeFileSync(join(root, 'assets'), 'block directory creation')
expect(() => database.updateMagicNoteEntry({ entryId: entry.id, expectedRevision: 0, content: rich, plainText: '' })).toThrow()
expect(readFileSync(path)).toEqual(before)
rmSync(join(root, 'assets'))
expect(database.getMagicNoteEntry(entry.id).content).toEqual(entry.content)
rmSync(path)
expect(() => database.getMagicNoteEntry(entry.id)).toThrow('ENOENT')
database.close()
expect(() => database.initialize(directory)).toThrow('ENOENT')
})
it('updates a PDF canvas repeatedly without copying PDF bytes to SQLite, and collects only unreferenced assets', () => {
const { database, sql, directory, path } = setup()
const content = canvas(1024 * 1024)
const note = database.createMagicNote({ title: 'PDF', content })
let entry = note.entries[0]!
const root = join(directory, 'notes', note.id)
const assetFiles = readdirSync(join(root, 'assets'))
const pdf = assetFiles.find((name) => name.endsWith('.pdf'))!
const pdfPath = join(root, 'assets', pdf)
const originalMtime = statSync(pdfPath).mtimeMs
sql.exec('PRAGMA wal_checkpoint(TRUNCATE)')
const bytesBefore = statSync(path).size
for (let index = 0; index < 20; index++) {
const next = { ...content, pages: [{ ...content.pages[0]!, objects: [{ type: 'IText', text: `Edit ${index}`, left: index }] }] }
entry = database.updateMagicNoteEntry({ entryId: entry.id, expectedRevision: entry.revision, content: next, plainText: '' }).entries[0]!
}
expect(statSync(pdfPath).mtimeMs).toBe(originalMtime)
expect(readdirSync(join(root, 'assets'))).toEqual(assetFiles)
expect(statSync(join(root, 'entries', `${entry.id}.json`)).size).toBeLessThan(5000)
expect(statSync(path).size + statSync(`${path}-wal`).size - bytesBefore).toBeLessThan(2 * 1024 * 1024)
const todo = database.listMagicTodos()[0]!
database.updateMagicTodo({ todoId: todo.id, expectedRevision: todo.revision, completed: true })
const saved = database.getMagicNoteEntry(entry.id)
database.close()
database.initialize(directory)
expect(database.getMagicNoteEntry(entry.id)).toEqual(saved)
database.updateMagicNoteEntry({ entryId: entry.id, expectedRevision: saved.revision, content: { version: 1, ops: [{ insert: 'No PDF\n' }] }, plainText: '' })
expect(readdirSync(join(root, 'assets'))).toEqual([])
})
it('cleans orphan files on reopening without removing another persisted record', () => {
const { database, directory } = setup()
const note = database.createMagicNote({ title: 'Keep', content: rich })
const root = join(directory, 'notes', note.id)
const orphan = join(directory, 'notes', randomUUID())
mkdirSync(orphan)
writeFileSync(join(root, 'entries', `${randomUUID()}.json`), '{}')
writeFileSync(join(root, 'assets', 'unused.bin'), 'unused')
database.close()
database.initialize(directory)
expect(existsSync(orphan)).toBe(false)
expect(readdirSync(join(root, 'entries'))).toEqual([`${note.entries[0]!.id}.json`])
expect(readdirSync(join(root, 'assets'))).toHaveLength(3)
expect(database.getMagicNote(note.id).entries[0]!.content).toEqual(rich)
})
})