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<AssistantDatabase> {
  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'
    })
    expect(updated).toMatchObject({
      name: '产品发布 2',
      defaultWorkMode: 'execute'
    })
    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', '代码审查']
    })
    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: '可以拆成可检查的发布步骤。'
      })
    ])
    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('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()
  })
})
