Files
goodbuddy/src/main/magic-notes/magic-note-storage.test.ts
T
mesalogo 1094433a7d feat: connect conversations with Magic Notes
Let users retain useful assistant replies or complete conversations without
leaving their chat workflow. Preview and edit captured text, choose an existing
note or atomically create a note with its first entry, and return to the source
conversation or message from saved records.

Add an application-scoped Notes workbar tab with title/body search and quick
text appends. Preserve drafts across conversation switches and guard closing,
replacement and disabling. Schema 48 stores optional source metadata without
rewriting file-backed bodies; source deletion retains the saved note. Older
clients cannot reopen an upgraded database without a pre-upgrade backup.

Release note: 可将助手回答或完整会话预览、编辑后加入魔法笔记,并从笔记返回原会话或消息。右侧快速笔记支持搜索和追加文字,切换会话保留草稿;采集本身不调用模型,保存后自动评论仍遵循已有设置。

Validation: isolated HEAD-based candidate passed 22 files / 930 focused tests,
plus 20 gateway tests after including its create-result fixture adjustment.
Real Electron App/preload/IPC/SQLite acceptance passed for all 502 historical
messages, exact-entry/source navigation, native keyboard input in a 214px
panel, reopen and source-deletion retention, with zero capture model calls.
Repository lint and web TypeScript passed. Cached whitespace and secret-pattern
checks passed; native-client changes and unrelated IPC extraction are excluded.

Validation limits: the isolated source tree reuses installed node_modules,
where a parallel task upgraded DSH from the committed 0.1.2-rc.1 to 0.1.7-rc.2.
Full typecheck reports 14 DSH errors in three unchanged files; the full-suite
attempt with --bail=1 also fails in deepseek-harness-host.test.ts. These are not
all-green repository or release-package results. Earlier execution-time full
suite failures and fixture repairs remain recorded in the feature progress doc.
2026-09-27 08:17:54 +08:00

444 lines
27 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'
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 })
})
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('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('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]!
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: entry.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: 1 })
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)
})
})