import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { DatabaseSync } from 'node:sqlite' import { afterEach, describe, expect, it, vi } from 'vitest' import { AssistantDatabase } from './assistant-database' const temporaryDirectories: string[] = [] const channelDefaultProfileId = '00000000-0000-4000-8000-000000000001' afterEach(async () => { vi.useRealTimers() await Promise.all( temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }) ) ) }) async function createDatabase(): Promise { const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-assistant-')) temporaryDirectories.push(directory) const database = new AssistantDatabase(join(directory, 'assistant.sqlite')) database.initialize('C:\\Workspace') return database } describe('AssistantDatabase', () => { it('rejects a newer unsupported schema without changing its version', async () => { const directory = await mkdtemp( join(tmpdir(), 'goodbuddy-assistant-future-') ) temporaryDirectories.push(directory) const databasePath = join(directory, 'assistant.sqlite') const initial = new AssistantDatabase(databasePath) initial.initialize('C:\\Workspace') initial.close() const future = new DatabaseSync(databasePath) future.exec('PRAGMA user_version = 99;') future.close() const downgraded = new AssistantDatabase(databasePath) expect(() => downgraded.initialize('C:\\Workspace')).toThrow( '不支持助理数据库版本 99' ) const unchanged = new DatabaseSync(databasePath) expect( ( unchanged.prepare('PRAGMA user_version').get() as { user_version: number } ).user_version ).toBe(99) unchanged.close() }) it('lists projects by creation time with newer projects last', async () => { vi.useFakeTimers() vi.setSystemTime(new Date('2026-08-07T00:00:00.000Z')) const database = await createDatabase() const defaultProject = database.listProjects()[0]! vi.setSystemTime(new Date('2026-08-07T00:01:00.000Z')) const secondProject = database.createProject({ name: '第二项目', description: '', rootPath: 'C:\\Second', defaultWorkMode: 'ask' }) vi.setSystemTime(new Date('2026-08-07T00:02:00.000Z')) const thirdProject = database.createProject({ name: '第三项目', description: '', rootPath: 'C:\\Third', defaultWorkMode: 'execute' }) database.updateProject(secondProject.id, { name: '第二项目(已更新)', description: '', rootPath: 'C:\\Second', defaultWorkMode: 'ask' }) expect(database.listProjects().map((project) => project.id)).toEqual([ defaultProject.id, secondProject.id, thirdProject.id ]) expect( database.listProjects(true).map((project) => project.id) ).toEqual([ defaultProject.id, secondProject.id, thirdProject.id ]) database.close() }) it('migrates existing databases to schema version 17', async () => { const directory = await mkdtemp( join(tmpdir(), 'goodbuddy-assistant-migration-') ) temporaryDirectories.push(directory) const databasePath = join(directory, 'assistant.sqlite') const initial = new AssistantDatabase(databasePath) initial.initialize('C:\\Workspace') initial.close() const oldDatabase = new DatabaseSync(databasePath) oldDatabase.exec(` DROP TABLE model_usage_calls; ALTER TABLE projects DROP COLUMN runtime_selection_json; PRAGMA user_version = 3; `) oldDatabase.close() const migrated = new AssistantDatabase(databasePath) migrated.initialize('C:\\Workspace') migrated.close() const current = new DatabaseSync(databasePath) expect( ( current.prepare('PRAGMA user_version').get() as { user_version: number } ).user_version ).toBe(17) expect( current .prepare( `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'model_usage_calls'` ) .get() ).toEqual({ name: 'model_usage_calls' }) expect( current .prepare('PRAGMA table_info(projects)') .all() ).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'runtime_selection_json' }) ]) ) const foreignKeys = current .prepare('PRAGMA foreign_key_list(model_usage_calls)') .all() as Array<{ table: string from: string to: string on_delete: string }> expect(foreignKeys).toEqual( expect.arrayContaining([ expect.objectContaining({ table: 'tasks', from: 'request_id', to: 'id', on_delete: 'CASCADE' }) ]) ) expect( current .prepare( `SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('tasks_status_idx', 'messages_state_idx') ORDER BY name` ) .all() ).toEqual([ { name: 'messages_state_idx' }, { name: 'tasks_status_idx' } ]) expect( ( current.prepare('PRAGMA table_info(tasks)').all() as Array<{ name: string }> ).some((column) => column.name === 'visible') ).toBe(true) expect( ( current .prepare('PRAGMA table_info(magic_note_entries)') .all() as Array<{ name: string }> ).some((column) => column.name === 'image_bytes') ).toBe(true) expect( current .prepare( `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'magic_todos'` ) .get() ).toEqual({ name: 'magic_todos' }) current.close() }) it('idempotently migrates version 5 databases to computer control audit schema', async () => { const directory = await mkdtemp( join(tmpdir(), 'goodbuddy-control-audit-migration-') ) temporaryDirectories.push(directory) const databasePath = join(directory, 'assistant.sqlite') const initial = new AssistantDatabase(databasePath) initial.initialize('C:\\Workspace') initial.close() const versionFive = new DatabaseSync(databasePath) versionFive.exec(` DROP TABLE computer_control_actions; PRAGMA user_version = 5; `) versionFive.close() for (let attempt = 0; attempt < 2; attempt += 1) { const migrated = new AssistantDatabase(databasePath) migrated.initialize('C:\\Workspace') migrated.close() } const current = new DatabaseSync(databasePath) expect( ( current.prepare('PRAGMA user_version').get() as { user_version: number } ).user_version ).toBe(17) expect( current .prepare( `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'computer_control_actions'` ) .get() ).toEqual({ name: 'computer_control_actions' }) expect( current .prepare( `SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'computer_control_actions_recent_idx'` ) .get() ).toEqual({ name: 'computer_control_actions_recent_idx' }) expect( current .prepare( 'PRAGMA foreign_key_list(computer_control_actions)' ) .all() ).toEqual( expect.arrayContaining([ expect.objectContaining({ table: 'tasks', from: 'task_id', to: 'id', on_delete: 'CASCADE' }) ]) ) current.close() }) it('backfills checklist todos when migrating existing magic notes', async () => { const directory = await mkdtemp( join(tmpdir(), 'goodbuddy-magic-todo-migration-') ) temporaryDirectories.push(directory) const databasePath = join(directory, 'assistant.sqlite') const initial = new AssistantDatabase(databasePath) initial.initialize('C:\\Workspace') const note = initial.createMagicNote({ title: '迁移笔记' }) initial.createMagicNoteEntry({ noteId: note.id, content: { version: 1, ops: [ { insert: '迁移待办' }, { insert: '\n', attributes: { list: 'unchecked' } } ] }, plainText: '迁移待办' }) initial.close() const legacy = new DatabaseSync(databasePath) legacy.exec(` DELETE FROM magic_todos; PRAGMA user_version = 9; `) legacy.close() const migrated = new AssistantDatabase(databasePath) migrated.initialize('C:\\Workspace') expect(migrated.listMagicTodos()).toEqual([ expect.objectContaining({ noteId: note.id, source: 'note', title: '迁移待办', completed: false }) ]) migrated.close() }) it('makes existing notes global and migrates manual todos into one note', async () => { const directory = await mkdtemp( join(tmpdir(), 'goodbuddy-global-magic-notes-migration-') ) temporaryDirectories.push(directory) const databasePath = join(directory, 'assistant.sqlite') const initial = new AssistantDatabase(databasePath) initial.initialize('C:\\Workspace') const project = initial.listProjects()[0]! const note = initial.createMagicNote({ title: '原项目笔记' }) initial.close() const legacy = new DatabaseSync(databasePath) const now = '2026-08-10T00:00:00.000Z' legacy .prepare('UPDATE magic_notes SET project_id = ? WHERE id = ?') .run(project.id, note.id) legacy .prepare( `INSERT INTO magic_todos (id, project_id, note_id, entry_id, source_index, source, title, instructions, completed, comments_json, analyzed_at, revision, created_at, updated_at) VALUES (?, ?, NULL, NULL, NULL, 'manual', ?, ?, 1, '[]', NULL, 0, ?, ?)` ) .run( '00000000-0000-4000-8000-000000000099', project.id, '旧手动待办', '保留的说明', now, now ) legacy.exec('PRAGMA user_version = 16') legacy.close() const migrated = new AssistantDatabase(databasePath) migrated.initialize('C:\\Workspace') expect(migrated.listMagicNotes()).toEqual( expect.arrayContaining([ expect.objectContaining({ id: note.id, title: '原项目笔记' }), expect.objectContaining({ title: '迁入的待办' }) ]) ) expect(migrated.listMagicTodos()).toEqual([ expect.objectContaining({ source: 'note', title: '旧手动待办', instructions: '保留的说明', completed: true, noteTitle: '迁入的待办' }) ]) migrated.close() }) it('creates a default project and persists project updates', async () => { const database = await createDatabase() const [defaultProject] = database.listProjects() expect(defaultProject).toMatchObject({ name: '默认项目', rootPath: 'C:\\Workspace', defaultWorkMode: 'ask', status: 'active' }) expect(database.listExperts()).toHaveLength(3) const project = database.createProject({ name: '产品发布', description: '发布资料和任务', rootPath: 'C:\\Release', defaultWorkMode: 'plan' }) expect(database.listProjects()).toHaveLength(2) const updated = database.updateProject(project.id, { name: '产品发布 2', description: '更新后的项目', rootPath: 'C:\\Release', defaultWorkMode: 'execute', runtimeSelection: { provider: 'continue' } }) expect(updated).toMatchObject({ name: '产品发布 2', defaultWorkMode: 'execute', runtimeSelection: { provider: 'continue' } }) database.setProjectArchived(project.id, true) expect(database.listProjects()).toHaveLength(1) expect(database.listProjects(true)).toEqual( expect.arrayContaining([ expect.objectContaining({ id: project.id, status: 'archived' }) ]) ) database.close() }) it('idempotently creates protected channel projects by channel identity', async () => { const database = await createDatabase() const sameName = database.createProject({ name: '微信 ClawBot', description: '普通同名项目', rootPath: 'C:\\Ordinary', defaultWorkMode: 'execute' }) const first = database.ensureChannelProjects( 'C:\\Users\\test', channelDefaultProfileId ) const second = database.ensureChannelProjects( 'C:\\Ignored', channelDefaultProfileId ) expect(first).toEqual([ expect.objectContaining({ name: '微信 ClawBot', rootPath: 'C:\\Users\\test', defaultWorkMode: 'ask', runtimeSelection: { provider: 'model', profileId: channelDefaultProfileId }, kind: 'channel', channel: 'weixin' }), expect.objectContaining({ kind: 'channel', channel: 'wecom' }), expect.objectContaining({ kind: 'channel', channel: 'dingtalk' }) ]) expect(second.map((project) => project.id)).toEqual( first.map((project) => project.id) ) expect(database.getProject(sameName.id)).toMatchObject({ kind: 'user', channel: undefined, rootPath: 'C:\\Ordinary' }) const weixin = first[0]! const updated = database.updateProject(weixin.id, { name: '不可重命名', description: '更新后的通道说明', rootPath: 'C:\\Remote', defaultWorkMode: 'execute', runtimeSelection: { provider: 'opencode', profileId: '00000000-0000-4000-8000-000000000019' } }) expect(updated).toMatchObject({ name: '微信 ClawBot', description: '更新后的通道说明', rootPath: 'C:\\Remote', defaultWorkMode: 'execute', runtimeSelection: { provider: 'opencode', profileId: '00000000-0000-4000-8000-000000000019' } }) expect(() => database.updateProject(weixin.id, { name: weixin.name, description: weixin.description, rootPath: ' ', defaultWorkMode: 'execute' }) ).toThrow('通道项目必须设置默认工作目录') expect(() => database.setProjectArchived(weixin.id, true) ).toThrow('系统通道项目不能归档') expect(() => database.deleteProject(weixin.id, weixin.name) ).toThrow('系统通道项目不能删除') database.close() }) it('persists one protected remote conversation per channel identity', async () => { const database = await createDatabase() const project = database.ensureChannelProjects( 'C:\\Users\\test', channelDefaultProfileId )[0]! const first = database.getOrCreateRemoteConversation({ projectId: project.id, channel: 'weixin', accountId: 'default', externalConversationId: 'remote-user-1', conversationType: 'direct', title: '微信 ClawBot · ****0001', accountDisplay: '发送者 ****0001', runtimeSelection: { provider: 'continue' } }) const second = database.getOrCreateRemoteConversation({ projectId: project.id, channel: 'weixin', accountId: 'default', externalConversationId: 'remote-user-1', conversationType: 'direct', title: '微信 ClawBot · ****0001', accountDisplay: '发送者 ****0001', runtimeSelection: { provider: 'continue' } }) expect(second.id).toBe(first.id) database.appendRemoteConversationMessage({ conversationId: first.id, role: 'user', content: '请分析状态', attachments: [ { id: '00000000-0000-4000-8000-000000000090', name: '状态.txt', size: 12, preview: '状态', kind: 'text' } ], status: '微信 ClawBot · 对话' }) database.appendRemoteConversationMessage({ conversationId: first.id, role: 'assistant', content: '状态正常', artifactIds: [ '00000000-0000-4000-8000-000000000091' ], status: '微信 ClawBot · 已完成' }) expect(database.getConversation(first.id)).toMatchObject({ projectId: project.id, runtimeSelection: { provider: 'continue' }, remote: { channel: 'weixin', accountDisplay: '发送者 ****0001', conversationType: 'direct' }, messages: [ { role: 'user', content: '请分析状态', attachments: [ expect.objectContaining({ name: '状态.txt' }) ], status: '微信 ClawBot · 对话' }, { role: 'assistant', content: '状态正常', artifactIds: [ '00000000-0000-4000-8000-000000000091' ], status: '微信 ClawBot · 已完成' } ] }) database.replaceConversations([]) expect(database.getConversation(first.id).remote?.channel).toBe( 'weixin' ) database.close() }) it('persists remote event deduplication and failed reply outbox state', async () => { const directory = await mkdtemp( join(tmpdir(), 'goodbuddy-channel-state-') ) temporaryDirectories.push(directory) const databasePath = join(directory, 'assistant.sqlite') const database = new AssistantDatabase(databasePath) database.initialize('C:\\Workspace') expect(database.claimChannelEvent('weixin', 'event-1')).toBe(true) expect(database.claimChannelEvent('weixin', 'event-1')).toBe(false) expect(database.claimChannelEvent('dingtalk', 'event-1')).toBe(true) const entry = database.enqueueChannelResult({ channel: 'weixin', eventId: 'event-1', conversationId: 'conversation-1', recipientId: 'sender-1', status: 'completed', output: '已完成' }) database.markChannelResult(entry.id, 'failed') expect(database.listUndeliveredChannelResults()).toEqual([ { ...entry, state: 'failed', attempts: 1 } ]) database.markChannelResult(entry.id, 'delivered') expect(database.listUndeliveredChannelResults()).toEqual([]) database.close() const reopened = new AssistantDatabase(databasePath) reopened.initialize('C:\\Workspace') expect(reopened.claimChannelEvent('weixin', 'event-1')).toBe( false ) reopened.close() }) it('safely deletes a confirmed project and its scoped data', async () => { const database = await createDatabase() const project = database.createProject({ name: '待删除项目', description: '删除测试', rootPath: 'C:\\Delete', defaultWorkMode: 'execute' }) const conversationId = '00000000-0000-4000-8000-000000000111' const taskId = '00000000-0000-4000-8000-000000000211' database.replaceConversations([ { id: conversationId, projectId: project.id, title: '项目对话', updatedAt: Date.now(), messages: [] } ]) database.createTask({ id: taskId, projectId: project.id, conversationId, title: '项目任务', instructions: '执行任务', workMode: 'execute' }) database.createTextArtifact({ projectId: project.id, taskId, title: '项目成果', content: '内容' }) database.createMemory({ scope: 'project', scopeId: project.id, type: 'fact', content: '项目记忆' }) database.createSchedule({ projectId: project.id, title: '项目计划', prompt: '执行计划', workMode: 'ask', recurrence: 'daily', nextRunAt: '2026-08-08T00:00:00.000Z' }) expect(() => database.deleteProject(project.id, project.name) ).toThrow('项目仍有进行中的任务') database.updateTaskStatus(taskId, 'completed') expect(() => database.deleteProject(project.id, '错误名称') ).toThrow('项目名称确认不匹配') database.deleteProject(project.id, project.name) expect( database.listProjects(true).some((item) => item.id === project.id) ).toBe(false) expect( database.listConversations().some( (conversation) => conversation.projectId === project.id ) ).toBe(false) expect( database.listTasks().some((task) => task.projectId === project.id) ).toBe(false) expect(database.listArtifacts(project.id)).toEqual([]) expect(database.listSchedules(project.id)).toEqual([]) expect( database .listMemories(project.id) .some((memory) => memory.scopeId === project.id) ).toBe(false) expect(database.listProjects()).toHaveLength(1) database.close() }) it('does not delete the final active project', async () => { const database = await createDatabase() const project = database.listProjects()[0]! expect(() => database.deleteProject(project.id, project.name) ).toThrow('至少需要保留一个可用项目') expect(database.listProjects()).toHaveLength(1) database.close() }) it('creates, updates, and soft-deletes expert roles', async () => { const database = await createDatabase() const expert = database.createExpert({ name: '代码审查专家', description: '检查代码正确性', systemInstructions: 'Review code for actionable bugs.', routingKeywords: [' CODE ', 'code', '代码审查'] }) expect(expert.routingKeywords).toEqual(['code', '代码审查']) const updated = database.updateExpert(expert.id, { name: '高级代码审查专家', description: '检查正确性和安全性', systemInstructions: 'Review correctness and security risks.', routingKeywords: ['security', '安全审查'] }) expect(updated).toMatchObject({ id: expert.id, name: '高级代码审查专家', description: '检查正确性和安全性', systemInstructions: 'Review correctness and security risks.', routingKeywords: ['security', '安全审查'], enabled: true }) database.removeExpert(expert.id) expect( database.listExperts().some((item) => item.id === expert.id) ).toBe(false) expect(() => database.getExpert(expert.id)).toThrow( '专家不存在或已停用' ) database.close() }) it('roundtrips expert model profiles and tolerates malformed model policies', async () => { const directory = await mkdtemp( join(tmpdir(), 'goodbuddy-expert-model-policy-') ) temporaryDirectories.push(directory) const databasePath = join(directory, 'assistant.sqlite') const firstModelProfileId = '00000000-0000-4000-8000-000000000401' const secondModelProfileId = '00000000-0000-4000-8000-000000000402' const database = new AssistantDatabase(databasePath) database.initialize('C:\\Workspace') const expert = database.createExpert({ name: '模型绑定专家', description: '验证模型策略持久化', systemInstructions: 'Use the assigned model connection.', modelProfileId: firstModelProfileId, routingKeywords: ['模型绑定'] }) expect(expert.modelProfileId).toBe(firstModelProfileId) expect( database.listExperts().find((item) => item.id === expert.id) ).toMatchObject({ modelProfileId: firstModelProfileId, routingKeywords: ['模型绑定'] }) const updated = database.updateExpert(expert.id, { name: expert.name, description: expert.description, systemInstructions: expert.systemInstructions, modelProfileId: secondModelProfileId, routingKeywords: expert.routingKeywords }) expect(updated.modelProfileId).toBe(secondModelProfileId) database.close() const persisted = new DatabaseSync(databasePath) expect( JSON.parse( ( persisted .prepare( 'SELECT model_policy_json FROM experts WHERE id = ?' ) .get(expert.id) as { model_policy_json: string } ).model_policy_json ) ).toEqual({ modelProfileId: secondModelProfileId }) expect( ( persisted.prepare('PRAGMA table_info(experts)').all() as Array<{ name: string }> ).some((column) => column.name === 'model_profile_id') ).toBe(false) persisted .prepare( 'UPDATE experts SET model_policy_json = ? WHERE id = ?' ) .run('{malformed-json', expert.id) persisted.close() const reopened = new AssistantDatabase(databasePath) reopened.initialize('C:\\Workspace') const recoveredExpert = reopened .listExperts() .find((item) => item.id === expert.id) reopened.close() expect(recoveredExpert).toMatchObject({ id: expert.id, routingKeywords: ['模型绑定'] }) expect(recoveredExpert?.modelProfileId).toBeUndefined() }) it('persists task lifecycle and events', async () => { const database = await createDatabase() const project = database.listProjects()[0]! const taskId = '00000000-0000-4000-8000-000000000201' database.createTask({ id: taskId, projectId: project.id, conversationId: 'conversation-1', title: '整理发布说明', instructions: '根据本次变更整理说明', workMode: 'execute' }) expect(database.listTasks()[0]).toMatchObject({ id: taskId, status: 'running', projectId: project.id }) const expert = database.listExperts()[0]! const childTaskId = '00000000-0000-4000-8000-000000000202' database.createTask({ id: childTaskId, projectId: project.id, conversationId: 'conversation-1', parentTaskId: taskId, expertId: expert.id, routingMode: 'smart', title: '研究子任务', instructions: '只读分析', workMode: 'ask', origin: 'subagent', status: 'queued' }) expect(database.listTasks()[0]).toMatchObject({ id: childTaskId, parentTaskId: taskId, expertId: expert.id, routingMode: 'smart', status: 'queued' }) database.updateTaskStatus(taskId, 'waiting_approval') expect( database.listTasks().find((task) => task.id === taskId) ).toMatchObject({ status: 'waiting_approval' }) database.updateTaskStatus(taskId, 'completed') expect( database.listTasks().find((task) => task.id === taskId) ).toMatchObject({ status: 'completed', completedAt: expect.any(String) }) const artifact = database.createTextArtifact({ projectId: project.id, taskId, title: '发布说明', content: '# 发布说明\n\n内容' }) expect(database.listArtifacts(project.id)).toEqual([ expect.objectContaining({ id: artifact.id, kind: 'markdown', content: '# 发布说明\n\n内容' }) ]) const memory = database.createMemory({ scope: 'project', scopeId: project.id, type: 'preference', content: '使用简洁中文回复' }) expect(database.listMemories(project.id)).toEqual([ expect.objectContaining({ id: memory.id, status: 'confirmed', content: '使用简洁中文回复' }) ]) database.removeMemory(memory.id) expect(database.listMemories(project.id)).toEqual([]) const schedule = database.createSchedule({ projectId: project.id, title: '每日摘要', prompt: '总结今天的任务状态', workMode: 'ask', recurrence: 'daily', nextRunAt: '2026-07-31T00:00:00.000Z' }) expect( database.claimDueSchedules(new Date('2026-07-31T00:01:00.000Z')) ).toEqual([expect.objectContaining({ id: schedule.id })]) expect(database.listSchedules(project.id)[0]).toMatchObject({ id: schedule.id, nextRunAt: '2026-08-01T00:00:00.000Z', lastRunAt: '2026-07-31T00:01:00.000Z' }) const overdue = database.createSchedule({ projectId: project.id, title: '过期摘要', prompt: '总结任务状态', workMode: 'ask', recurrence: 'daily', nextRunAt: '2025-07-31T00:00:00.000Z' }) database.claimDueSchedules( new Date('2026-07-31T00:01:00.000Z') ) expect( database .listSchedules(project.id) .find((item) => item.id === overdue.id) ).toMatchObject({ nextRunAt: '2026-08-01T00:00:00.000Z' }) database.close() }) it('durably interrupts active tasks with completion times and audit events on startup', async () => { const directory = await mkdtemp( join(tmpdir(), 'goodbuddy-assistant-recovery-') ) temporaryDirectories.push(directory) const databasePath = join(directory, 'assistant.sqlite') const runningTaskId = '00000000-0000-4000-8000-000000000202' const approvalTaskId = '00000000-0000-4000-8000-000000000203' const initial = new AssistantDatabase(databasePath) initial.initialize('C:\\Workspace') initial.createTask({ id: runningTaskId, title: '运行中的任务', instructions: '等待启动恢复', workMode: 'execute' }) initial.createTask({ id: approvalTaskId, title: '等待审批的任务', instructions: '等待启动恢复', workMode: 'execute' }) initial.updateTaskStatus(approvalTaskId, 'waiting_approval') initial.close() const recovered = new AssistantDatabase(databasePath) recovered.initialize('C:\\Workspace') const recoveredTasks = recovered .listTasks() .filter((task) => [runningTaskId, approvalTaskId].includes(task.id) ) expect(recoveredTasks).toHaveLength(2) expect(recoveredTasks).toEqual( expect.arrayContaining([ expect.objectContaining({ id: runningTaskId, status: 'interrupted', completedAt: expect.any(String), error: '应用退出时任务仍在运行' }), expect.objectContaining({ id: approvalTaskId, status: 'interrupted', completedAt: expect.any(String), error: '应用退出时任务仍在运行' }) ]) ) recovered.close() const reopenedAgain = new AssistantDatabase(databasePath) reopenedAgain.initialize('C:\\Workspace') expect( reopenedAgain .listTasks() .filter((task) => [runningTaskId, approvalTaskId].includes(task.id) ) ).toEqual(recoveredTasks) reopenedAgain.close() const durable = new DatabaseSync(databasePath) const statusEvents = durable .prepare( `SELECT task_id, payload_json FROM task_events WHERE task_id IN (?, ?) AND kind = 'status' ORDER BY task_id, id` ) .all(runningTaskId, approvalTaskId) as Array<{ task_id: string payload_json: string }> const recoveryEvents = statusEvents .map((event) => ({ taskId: event.task_id, payload: JSON.parse(event.payload_json) as { status: string error?: string } })) .filter((event) => event.payload.status === 'interrupted') expect(recoveryEvents).toEqual([ { taskId: runningTaskId, payload: { status: 'interrupted', error: '应用退出时任务仍在运行' } }, { taskId: approvalTaskId, payload: { status: 'interrupted', error: '应用退出时任务仍在运行' } } ]) durable.close() }) it('replaces and restores bounded conversation snapshots', async () => { const database = await createDatabase() const project = database.listProjects()[0]! const conversationId = '00000000-0000-4000-8000-000000000211' database.replaceConversations([ { id: conversationId, projectId: project.id, runtimeSelection: { provider: 'model', profileId: '00000000-0000-4000-8000-000000000299' }, title: '发布讨论', updatedAt: 1_775_000_000_000, messages: [ { id: '00000000-0000-4000-8000-000000000212', role: 'user', content: '整理发布说明', createdAt: 1_775_000_000_000, state: 'complete', attachments: [ { id: '00000000-0000-4000-8000-000000000220', name: '发布清单.md', size: 2_048, preview: '发布前检查项', kind: 'text' }, { id: '00000000-0000-4000-8000-000000000221', name: '发布页面.png', size: 4_096, preview: '1280 × 720', kind: 'image', thumbnailUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB' } ] }, { id: '00000000-0000-4000-8000-000000000213', role: 'assistant', content: '处理中', reasoning: '先分析发布范围', blocks: [ { id: '00000000-0000-4000-8000-000000000217', type: 'reasoning', content: '先分析发布范围' }, { id: '00000000-0000-4000-8000-000000000218', type: 'tool', tool: { callId: 'call-1', name: 'read', state: 'running', summary: 'OpenCode 工具:read' } }, { id: '00000000-0000-4000-8000-000000000219', type: 'text', content: '处理中' } ], createdAt: 1_775_000_001_000, state: 'streaming', artifactIds: [ '00000000-0000-4000-8000-000000000216' ], sourceReferences: [ { libraryId: '00000000-0000-4000-8000-000000000214', libraryName: '产品知识', documentId: '00000000-0000-4000-8000-000000000215', documentName: '发布说明.md', sourceName: '发布目录', snippet: '发布前需要完成验证。', rank: -0.03, retrievalChannels: ['fts', 'vector'] } ] } ] } ]) expect(database.listConversations()).toEqual([ expect.objectContaining({ id: conversationId, projectId: project.id, runtimeSelection: { provider: 'model', profileId: '00000000-0000-4000-8000-000000000299' }, messages: [ expect.objectContaining({ role: 'user', state: 'complete', attachments: [ expect.objectContaining({ name: '发布清单.md', kind: 'text' }), expect.objectContaining({ name: '发布页面.png', kind: 'image', thumbnailUrl: expect.stringContaining( 'data:image/png;base64,' ) }) ] }), expect.objectContaining({ role: 'assistant', state: 'error', status: expect.stringContaining('意外中断'), reasoning: '先分析发布范围', blocks: [ expect.objectContaining({ type: 'reasoning', content: '先分析发布范围' }), expect.objectContaining({ type: 'tool', tool: expect.objectContaining({ callId: 'call-1', state: 'interrupted' }) }), expect.objectContaining({ type: 'text', content: '处理中' }) ], artifactIds: [ '00000000-0000-4000-8000-000000000216' ], sourceReferences: [ expect.objectContaining({ documentName: '发布说明.md', retrievalChannels: ['fts', 'vector'] }) ] }) ] }) ]) database.replaceConversations([]) expect(database.listConversations()).toEqual([]) database.close() }) it('rebinds persisted conversations whose model profile was removed', async () => { const database = await createDatabase() const removedProfileId = '00000000-0000-4000-8000-000000000291' const defaultProfileId = '00000000-0000-4000-8000-000000000292' const runtimeProfileId = '00000000-0000-4000-8000-000000000293' const imageProfileId = '00000000-0000-4000-8000-000000000294' database.replaceConversations( ([ ['model', removedProfileId], ['opencode', removedProfileId], ['continue', removedProfileId], ['model', runtimeProfileId] ] as const).map(([provider, profileId], index) => ({ id: `00000000-0000-4000-8000-00000000030${index}`, runtimeSelection: { provider, profileId }, title: `对话 ${index}`, updatedAt: index + 1, messages: [] })) ) const channelProject = database.ensureChannelProjects( 'C:\\Users\\test', defaultProfileId )[0]! database.updateProject(channelProject.id, { name: channelProject.name, description: channelProject.description, rootPath: channelProject.rootPath, defaultWorkMode: channelProject.defaultWorkMode, runtimeSelection: { provider: 'opencode', profileId: runtimeProfileId } }) const imageChannelProject = database.ensureChannelProjects( 'C:\\Users\\test', defaultProfileId )[1]! database.updateProject(imageChannelProject.id, { name: imageChannelProject.name, description: imageChannelProject.description, rootPath: imageChannelProject.rootPath, defaultWorkMode: imageChannelProject.defaultWorkMode, runtimeSelection: { provider: 'model', profileId: imageProfileId } }) const automaticChannelProject = database.ensureChannelProjects( 'C:\\Users\\test', defaultProfileId )[2]! database.updateProject(automaticChannelProject.id, { name: automaticChannelProject.name, description: automaticChannelProject.description, rootPath: automaticChannelProject.rootPath, defaultWorkMode: automaticChannelProject.defaultWorkMode, runtimeSelection: { provider: 'auto' } }) const automaticRemoteConversation = database.getOrCreateRemoteConversation({ projectId: automaticChannelProject.id, channel: 'dingtalk', accountId: 'default', externalConversationId: 'legacy-auto-conversation', conversationType: 'direct', title: '钉钉 · 旧版自动后端', accountDisplay: '发送者 ****0001', runtimeSelection: { provider: 'auto' } }) expect( database.repairConversationRuntimeSelections({ modelProfiles: [ { id: defaultProfileId }, { id: runtimeProfileId }, { id: imageProfileId, protocol: 'openai-images-generations' } ], defaultModelProfileId: defaultProfileId, opencodeModelSource: { kind: 'profile', profileId: runtimeProfileId }, continueModelSource: { kind: 'platform' } }) ).toBe(7) expect( database .listConversations() .filter((conversation) => !conversation.remote) .sort((left, right) => left.title.localeCompare(right.title)) .map((conversation) => conversation.runtimeSelection) ).toEqual([ { provider: 'model', profileId: defaultProfileId }, { provider: 'opencode', profileId: runtimeProfileId }, { provider: 'continue' }, { provider: 'model', profileId: runtimeProfileId } ]) expect(database.getProject(channelProject.id).runtimeSelection).toEqual({ provider: 'opencode' }) expect( database.getProject(imageChannelProject.id).runtimeSelection ).toEqual({ provider: 'model', profileId: defaultProfileId }) expect( database.getProject(automaticChannelProject.id).runtimeSelection ).toEqual({ provider: 'model', profileId: defaultProfileId }) expect( database.getConversation( automaticRemoteConversation.id ).runtimeSelection ).toEqual({ provider: 'model', profileId: defaultProfileId }) database.close() }) it('durably interrupts active tool metadata during startup recovery', async () => { const directory = await mkdtemp( join(tmpdir(), 'goodbuddy-conversation-recovery-') ) temporaryDirectories.push(directory) const databasePath = join(directory, 'assistant.sqlite') const conversationId = '00000000-0000-4000-8000-000000000217' const messageId = '00000000-0000-4000-8000-000000000218' const cancelledMessageId = '00000000-0000-4000-8000-000000000219' const initial = new AssistantDatabase(databasePath) initial.initialize('C:\\Workspace') initial.replaceConversations([ { id: conversationId, title: '工具恢复', updatedAt: 1_775_000_000_000, messages: [ { id: messageId, role: 'assistant', content: '工具仍在运行', createdAt: 1_775_000_001_000, state: 'streaming', status: '正在执行工具', tools: [ { name: 'pending-tool', state: 'pending', summary: '等待调用' }, { name: 'running-tool', state: 'running', summary: '正在调用' }, { name: 'completed-tool', state: 'completed', summary: '调用完成' }, { name: 'failed-tool', state: 'failed', summary: '调用失败' } ] }, { id: cancelledMessageId, role: 'assistant', content: '请求已取消', createdAt: 1_775_000_002_000, state: 'error', status: '请求已取消', tools: [ { name: 'cancelled-tool', state: 'running', summary: '取消前仍在运行', error: 'runtime parser detail' } ] } ] } ]) initial.close() const recovered = new AssistantDatabase(databasePath) recovered.initialize('C:\\Workspace') expect(recovered.listConversations()[0]?.messages[0]).toMatchObject({ id: messageId, state: 'error', status: '上次运行意外中断,可以重新发送问题', tools: [ expect.objectContaining({ name: 'pending-tool', state: 'interrupted' }), expect.objectContaining({ name: 'running-tool', state: 'interrupted' }), expect.objectContaining({ name: 'completed-tool', state: 'completed' }), expect.objectContaining({ name: 'failed-tool', state: 'failed' }) ] }) expect(recovered.listConversations()[0]?.messages[1]).toMatchObject({ id: cancelledMessageId, state: 'error', status: '请求已取消', tools: [ expect.objectContaining({ name: 'cancelled-tool', state: 'interrupted', error: 'runtime parser detail' }) ] }) recovered.close() const durable = new DatabaseSync(databasePath) const row = durable .prepare( `SELECT state, metadata_json FROM messages WHERE id = ?` ) .get(messageId) as { state: string metadata_json: string } const metadata = JSON.parse(row.metadata_json) as { status?: string tools?: Array<{ name: string; state: string }> } expect(row.state).toBe('error') expect(metadata.status).toBe( '上次运行意外中断,可以重新发送问题' ) expect(metadata.tools?.map((tool) => tool.state)).toEqual([ 'interrupted', 'interrupted', 'completed', 'failed' ]) const cancelledRow = durable .prepare( `SELECT metadata_json FROM messages WHERE id = ?` ) .get(cancelledMessageId) as { metadata_json: string } expect( ( JSON.parse(cancelledRow.metadata_json) as { tools?: Array<{ state: string }> } ).tools?.[0]?.state ).toBe('interrupted') durable.close() }) it('loads image artifact content only when requested by id', async () => { const database = await createDatabase() const artifact = database.createInlineArtifact({ kind: 'image', title: '生成图片', mimeType: 'image/png', content: 'data:image/png;base64,iVBORw0KGgo=' }) expect( database.listArtifacts().find((item) => item.id === artifact.id) ).toMatchObject({ id: artifact.id, content: undefined }) expect(database.getArtifact(artifact.id)).toMatchObject({ id: artifact.id, content: 'data:image/png;base64,iVBORw0KGgo=' }) database.close() }) it('persists remote delegation results until delivery succeeds', async () => { const database = await createDatabase() const taskId = '00000000-0000-4000-8000-000000000221' database.saveDelegationResult(taskId, { status: 'completed', output: '远程结果' }) expect(database.listPendingDelegationResults()).toEqual([ { taskId, result: { status: 'completed', output: '远程结果' } } ]) database.markDelegationDelivered(taskId) expect(database.listPendingDelegationResults()).toEqual([]) expect(database.getDelegationDeliveryStatus(taskId)).toBe( 'delivered' ) database.close() }) it('upserts absolute token usage snapshots idempotently', async () => { const database = await createDatabase() const taskId = '00000000-0000-4000-8000-000000000301' database.createTask({ id: taskId, title: '统计令牌', instructions: '记录模型调用', workMode: 'ask' }) database.upsertModelUsageCall({ requestId: taskId, callId: 'call-1', runtime: 'opencode', provider: 'anthropic', model: 'claude-sonnet', input: 100, output: 20, cacheRead: 30, cacheWrite: 10 }) database.upsertModelUsageCall({ requestId: taskId, callId: 'call-1', runtime: 'opencode', provider: 'anthropic', model: 'claude-sonnet', input: 125, output: 25, cacheRead: 40, cacheWrite: 12 }) expect(database.getTokenUsageSummary()).toEqual({ totals: { callCount: 1, input: 125, output: 25, cacheRead: 40, cacheWrite: 12, totalTokens: 150 }, records: [ expect.objectContaining({ requestId: taskId, callCount: 1, input: 125, output: 25, cacheRead: 40, cacheWrite: 12, totalTokens: 150 }) ] }) expect(() => database.upsertModelUsageCall({ requestId: taskId, callId: 'negative', runtime: 'opencode', provider: 'anthropic', model: 'claude-sonnet', input: -1, output: 0, cacheRead: 0, cacheWrite: 0 }) ).toThrow('input must be a nonnegative safe integer') expect(() => database.upsertModelUsageCall({ requestId: taskId, callId: 'fractional', runtime: 'opencode', provider: 'anthropic', model: 'claude-sonnet', input: 0, output: 0.5, cacheRead: 0, cacheWrite: 0 }) ).toThrow('output must be a nonnegative safe integer') expect(() => database.upsertModelUsageCall({ requestId: taskId, callId: 'call-2', runtime: 'opencode', provider: 'anthropic', model: 'x'.repeat(501), input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }) ).toThrow('model must contain between 1 and 500 characters') database.close() }) it('aggregates token usage with project and conversation metadata', async () => { const database = await createDatabase() const firstProject = database.listProjects()[0]! const secondProject = database.createProject({ name: '第二项目', description: '', rootPath: 'C:\\Second', defaultWorkMode: 'ask' }) const firstConversationId = '00000000-0000-4000-8000-000000000311' const secondConversationId = '00000000-0000-4000-8000-000000000312' database.replaceConversations([ { id: firstConversationId, projectId: firstProject.id, title: '第一会话', updatedAt: 1_775_000_000_000, messages: [] }, { id: secondConversationId, projectId: secondProject.id, title: '第二会话', updatedAt: 1_775_000_001_000, messages: [] } ]) const firstTaskId = '00000000-0000-4000-8000-000000000321' const secondTaskId = '00000000-0000-4000-8000-000000000322' database.createTask({ id: firstTaskId, projectId: firstProject.id, conversationId: firstConversationId, title: '第一请求', instructions: '测试', workMode: 'ask' }) database.createTask({ id: secondTaskId, projectId: secondProject.id, conversationId: secondConversationId, title: '第二请求', instructions: '测试', workMode: 'ask' }) for (const usage of [ { requestId: firstTaskId, callId: 'call-1', runtime: 'opencode', provider: 'anthropic', model: 'claude-sonnet', input: 100, output: 40, cacheRead: 30, cacheWrite: 10 }, { requestId: firstTaskId, callId: 'call-2', runtime: 'opencode', provider: 'anthropic', model: 'claude-sonnet', input: 50, output: 20, cacheRead: 5, cacheWrite: 2 }, { requestId: firstTaskId, callId: 'call-3', runtime: 'continue', provider: 'openai', model: 'gpt-5', input: 80, output: 30, cacheRead: 0, cacheWrite: 0 }, { requestId: secondTaskId, callId: 'call-1', runtime: 'opencode', provider: 'anthropic', model: 'claude-sonnet', input: 25, output: 15, cacheRead: 7, cacheWrite: 3 } ]) { database.upsertModelUsageCall(usage) } const summary = database.getTokenUsageSummary() expect(summary.totals).toEqual({ callCount: 4, input: 255, output: 105, cacheRead: 42, cacheWrite: 15, totalTokens: 360 }) expect(summary.records).toHaveLength(3) expect(summary.records).toEqual( expect.arrayContaining([ expect.objectContaining({ requestId: firstTaskId, projectId: firstProject.id, projectName: firstProject.name, conversationId: firstConversationId, conversationTitle: '第一会话', runtime: 'opencode', provider: 'anthropic', model: 'claude-sonnet', callCount: 2, input: 150, output: 60, cacheRead: 35, cacheWrite: 12, totalTokens: 210 }), expect.objectContaining({ requestId: firstTaskId, runtime: 'continue', provider: 'openai', model: 'gpt-5', callCount: 1, totalTokens: 110 }), expect.objectContaining({ requestId: secondTaskId, projectId: secondProject.id, projectName: '第二项目', conversationId: secondConversationId, conversationTitle: '第二会话', callCount: 1, totalTokens: 40 }) ]) ) database.close() }) it('persists global magic notes and AI comments without todo proposals', async () => { const database = await createDatabase() const globalNote = database.createMagicNote({ title: '全局笔记' }) const secondNote = database.createMagicNote({ title: '第二篇笔记' }) expect(database.listMagicNotes()).toEqual( expect.arrayContaining([ expect.objectContaining({ id: globalNote.id, title: '全局笔记' }), expect.objectContaining({ id: secondNote.id, title: '第二篇笔记' }) ]) ) const withEntry = database.createMagicNoteEntry({ noteId: secondNote.id, content: { version: 1, ops: [ { insert: '整理发布清单', attributes: { bold: true } }, { insert: '\n' } ] }, plainText: '整理发布清单' }) const entry = withEntry.entries[0]! expect(withEntry).toMatchObject({ entryCount: 1, preview: '整理发布清单' }) expect(database.searchMagicNotes('发布', 5)).toEqual([ expect.objectContaining({ noteId: secondNote.id, noteTitle: '第二篇笔记', entryId: entry.id, content: '整理发布清单' }) ]) const analyzed = database.saveMagicNoteAnalysis({ entryId: entry.id, expectedRevision: entry.revision, comments: [ { id: '00000000-0000-4000-8000-000000000401', kind: 'suggestion', content: '可以拆成可检查的发布步骤。' } ] }) expect(analyzed.entries[0]!.comments).toEqual([ expect.objectContaining({ kind: 'suggestion', content: '可以拆成可检查的发布步骤。' }) ]) const reanalyzed = database.saveMagicNoteAnalysis({ entryId: entry.id, expectedRevision: analyzed.entries[0]!.revision, comments: [ { id: '00000000-0000-4000-8000-000000000402', kind: 'narrative', content: '可以继续补充目标读者和发布场景。', direction: 'expand', format: 'narrative' } ] }) expect(reanalyzed.entries[0]!.comments).toEqual([ expect.objectContaining({ content: '可以拆成可检查的发布步骤。' }), expect.objectContaining({ content: '可以继续补充目标读者和发布场景。', direction: 'expand', format: 'narrative', analyzedAt: expect.any(String) }) ]) expect(database.listTasks()).toEqual([]) database.close() }) it('synchronizes derived todos when note checklists change', async () => { const database = await createDatabase() const note = database.createMagicNote({ title: '发布笔记' }) const withEntry = database.createMagicNoteEntry({ noteId: note.id, content: { version: 1, ops: [ { insert: '核对发布材料' }, { insert: '\n', attributes: { list: 'unchecked' } }, { insert: '上传构建产物' }, { insert: '\n', attributes: { list: 'checked' } } ] }, plainText: '核对发布材料\n上传构建产物' }) const entry = withEntry.entries[0]! const noteTodos = database.listMagicTodos() expect(noteTodos).toEqual([ expect.objectContaining({ noteId: note.id, entryId: entry.id, source: 'note', title: '核对发布材料', completed: false }), expect.objectContaining({ source: 'note', title: '上传构建产物', completed: true }) ]) const updatedEntry = database.getMagicNote(note.id).entries[0]! database.updateMagicNoteEntry({ entryId: entry.id, expectedRevision: updatedEntry.revision, content: { version: 1, ops: [ { insert: '新增首项' }, { insert: '\n', attributes: { list: 'unchecked' } }, { insert: '上传构建产物' }, { insert: '\n', attributes: { list: 'unchecked' } }, { insert: '核对发布材料' }, { insert: '\n', attributes: { list: 'checked' } } ] }, plainText: '新增首项\n上传构建产物\n核对发布材料' }) const reordered = database.listMagicTodos() expect( reordered.find((todo) => todo.title === '核对发布材料') ).toMatchObject({ id: noteTodos[0]!.id, completed: true, sourceIndex: 2 }) expect( reordered.find((todo) => todo.title === '上传构建产物') ).toMatchObject({ id: noteTodos[1]!.id, completed: false, sourceIndex: 1 }) database.close() }) it('updates a derived todo and its source checklist together', async () => { const database = await createDatabase() const note = database.createMagicNote({ title: '发布笔记' }) database.createMagicNoteEntry({ noteId: note.id, content: { version: 1, ops: [ { insert: '核对发布材料' }, { insert: '\n', attributes: { list: 'unchecked' } } ] }, plainText: '核对发布材料' }) const todo = database.listMagicTodos()[0]! const updated = database.updateMagicTodo({ todoId: todo.id, completed: true, expectedRevision: todo.revision }) expect(updated).toMatchObject({ id: todo.id, completed: true, revision: todo.revision + 1 }) expect( database.getMagicNote(note.id).entries[0]!.content.ops ).toEqual([ { insert: '核对发布材料' }, { insert: '\n', attributes: { list: 'checked' } } ]) expect(() => database.updateMagicTodo({ todoId: todo.id, completed: false, expectedRevision: todo.revision }) ).toThrow('待办已被更新,请刷新后重试') database.close() }) it('protects magic note records from stale revisions', async () => { const database = await createDatabase() const note = database.createMagicNote({ title: '并发笔记' }) const withEntry = database.createMagicNoteEntry({ noteId: note.id, content: { version: 1, ops: [{ insert: '初始内容\n' }] }, plainText: '初始内容' }) const entry = withEntry.entries[0]! database.updateMagicNoteEntry({ entryId: entry.id, expectedRevision: entry.revision, content: { version: 1, ops: [{ insert: '新内容\n' }] }, plainText: '新内容' }) expect(() => database.updateMagicNoteEntry({ entryId: entry.id, expectedRevision: entry.revision, content: { version: 1, ops: [{ insert: '过期内容\n' }] }, plainText: '过期内容' }) ).toThrow('记录已被更新') database.close() }) it('clears private assistant content while preserving workspace configuration', async () => { const database = await createDatabase() const project = database.listProjects()[0]! database.createMemory({ scope: 'project', scopeId: project.id, type: 'fact', content: '待清除记忆' }) database.createSchedule({ projectId: project.id, title: '待清除任务', prompt: '总结', workMode: 'ask', recurrence: 'daily', nextRunAt: '2026-08-02T00:00:00.000Z' }) database.createHeartbeatConfig( { projectId: project.id, name: '待清除心跳', timezone: 'Asia/Shanghai', recurrence: { type: 'daily', localTime: '09:00' }, enabled: true, lookbackHours: 48, retentionDays: 90 }, new Date('2026-08-01T00:00:00.000Z') ) const taskId = '00000000-0000-4000-8000-000000000331' database.createTask({ id: taskId, projectId: project.id, title: '待清除用量', instructions: '测试', workMode: 'ask' }) database.upsertModelUsageCall({ requestId: taskId, callId: 'call-1', runtime: 'opencode', provider: 'anthropic', model: 'claude-sonnet', input: 10, output: 5, cacheRead: 2, cacheWrite: 1 }) database.createMagicNote({ title: '待清除笔记' }) expect(database.getTokenUsageSummary().totals.totalTokens).toBe(15) database.clearAssistantData() expect(database.listProjects()).toHaveLength(1) expect(database.listExperts()).toHaveLength(3) expect(database.listMemories(project.id)).toEqual([]) expect(database.listSchedules(project.id)).toEqual([]) expect(database.listHeartbeatConfigs(project.id)).toEqual([]) expect(database.listTasks()).toEqual([]) expect(database.listArtifacts(project.id)).toEqual([]) expect(database.listMagicNotes()).toEqual([]) expect(database.getTokenUsageSummary()).toEqual({ totals: { callCount: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0 }, records: [] }) database.close() }) })