@stonerzju/opencode 1.2.17 → 1.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/opencode +29 -157
- package/package.json +29 -29
- package/src/acp/agent.ts +4 -4
- package/src/acp/session.ts +1 -1
- package/src/agent/agent.ts +3 -3
- package/src/bun/index.ts +2 -2
- package/src/cli/cmd/acp.ts +3 -3
- package/src/cli/cmd/debug/file.ts +1 -1
- package/src/cli/cmd/github.ts +2 -2
- package/src/cli/cmd/pr.ts +1 -1
- package/src/cli/cmd/tui/app.tsx +24 -24
- package/src/cli/cmd/tui/attach.ts +3 -3
- package/src/cli/cmd/tui/component/dialog-agent.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-command.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +5 -5
- package/src/cli/cmd/tui/component/dialog-model.tsx +4 -4
- package/src/cli/cmd/tui/component/dialog-provider.tsx +4 -4
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +5 -5
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-skill.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-stash.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-status.tsx +2 -2
- package/src/cli/cmd/tui/component/dialog-tag.tsx +3 -3
- package/src/cli/cmd/tui/component/logo.tsx +2 -2
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +6 -6
- package/src/cli/cmd/tui/component/prompt/frecency.tsx +2 -2
- package/src/cli/cmd/tui/component/prompt/history.tsx +2 -2
- package/src/cli/cmd/tui/component/prompt/index.tsx +14 -14
- package/src/cli/cmd/tui/component/prompt/stash.tsx +2 -2
- package/src/cli/cmd/tui/component/textarea-keybindings.ts +1 -1
- package/src/cli/cmd/tui/component/tips.tsx +1 -1
- package/src/cli/cmd/tui/context/directory.ts +1 -1
- package/src/cli/cmd/tui/context/exit.tsx +1 -1
- package/src/cli/cmd/tui/context/keybind.tsx +2 -2
- package/src/cli/cmd/tui/context/kv.tsx +2 -2
- package/src/cli/cmd/tui/context/local.tsx +6 -6
- package/src/cli/cmd/tui/context/sync.tsx +4 -4
- package/src/cli/cmd/tui/context/theme/opencode.json +245 -0
- package/src/cli/cmd/tui/context/theme.tsx +2 -2
- package/src/cli/cmd/tui/context/tui-config.tsx +1 -1
- package/src/cli/cmd/tui/event.ts +2 -2
- package/src/cli/cmd/tui/routes/home.tsx +6 -6
- package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +6 -6
- package/src/cli/cmd/tui/routes/session/dialog-message.tsx +6 -6
- package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +2 -2
- package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +3 -3
- package/src/cli/cmd/tui/routes/session/header.tsx +5 -5
- package/src/cli/cmd/tui/routes/session/index.tsx +32 -32
- package/src/cli/cmd/tui/routes/session/permission.tsx +4 -4
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +4 -4
- package/src/cli/cmd/tui/thread.ts +9 -9
- package/src/cli/cmd/tui/ui/dialog-confirm.tsx +1 -1
- package/src/cli/cmd/tui/ui/dialog-help.tsx +2 -2
- package/src/cli/cmd/tui/ui/dialog-select.tsx +5 -5
- package/src/cli/cmd/tui/ui/dialog.tsx +3 -3
- package/src/cli/cmd/tui/ui/toast.tsx +1 -1
- package/src/cli/cmd/tui/util/editor.ts +3 -3
- package/src/cli/cmd/tui/util/transcript.ts +1 -1
- package/src/cli/cmd/tui/worker.ts +10 -10
- package/src/cli/error.ts +1 -1
- package/src/cli/ui.ts +1 -1
- package/src/cli/upgrade.ts +4 -4
- package/src/command/index.ts +1 -1
- package/src/config/config.ts +10 -10
- package/src/config/markdown.ts +1 -1
- package/src/config/migrate-tui-config.ts +5 -5
- package/src/config/paths.ts +4 -4
- package/src/config/tui.ts +4 -4
- package/src/control/control.sql.ts +1 -1
- package/src/control/index.ts +1 -1
- package/src/control-plane/adaptors/worktree.ts +1 -1
- package/src/control-plane/session-proxy-middleware.ts +1 -1
- package/src/control-plane/workspace.sql.ts +1 -1
- package/src/control-plane/workspace.ts +7 -7
- package/src/file/index.ts +1 -1
- package/src/file/ripgrep.ts +2 -2
- package/src/file/watcher.ts +5 -5
- package/src/format/formatter.ts +1 -1
- package/src/ide/index.ts +3 -3
- package/src/index.ts +1 -1
- package/src/installation/index.ts +3 -3
- package/src/lsp/client.ts +3 -3
- package/src/lsp/index.ts +3 -3
- package/src/mcp/index.ts +4 -4
- package/src/permission/index.ts +2 -2
- package/src/permission/next.ts +10 -10
- package/src/plugin/codex.ts +1 -1
- package/src/plugin/copilot.ts +2 -2
- package/src/plugin/index.ts +1 -1
- package/src/project/bootstrap.ts +2 -2
- package/src/project/instance.ts +4 -4
- package/src/project/project.sql.ts +1 -1
- package/src/project/project.ts +5 -5
- package/src/project/state.ts +1 -1
- package/src/project/vcs.ts +4 -4
- package/src/provider/auth.ts +4 -4
- package/src/provider/error.ts +1 -1
- package/src/provider/models-snapshot.ts +2 -0
- package/src/provider/models.ts +1 -1
- package/src/provider/provider.ts +2 -2
- package/src/provider/transform.ts +2 -2
- package/src/pty/index.ts +5 -5
- package/src/question/index.ts +5 -5
- package/src/server/event.ts +1 -1
- package/src/server/mdns.ts +1 -1
- package/src/server/routes/global.ts +3 -3
- package/src/server/routes/permission.ts +1 -1
- package/src/server/routes/pty.ts +1 -1
- package/src/server/routes/session.ts +4 -4
- package/src/server/routes/tui.ts +1 -1
- package/src/server/server.ts +3 -3
- package/src/session/compaction.ts +7 -7
- package/src/session/index.ts +10 -10
- package/src/session/instruction.ts +1 -1
- package/src/session/llm.ts +11 -11
- package/src/session/message-v2.ts +10 -10
- package/src/session/message.ts +1 -1
- package/src/session/processor.ts +10 -10
- package/src/session/prompt.ts +8 -8
- package/src/session/retry.ts +2 -2
- package/src/session/revert.ts +1 -1
- package/src/session/session.sql.ts +3 -3
- package/src/session/status.ts +3 -3
- package/src/session/summary.ts +5 -5
- package/src/session/system.ts +1 -1
- package/src/session/todo.ts +2 -2
- package/src/share/share-next.ts +7 -7
- package/src/share/share.sql.ts +1 -1
- package/src/shell/shell.ts +3 -3
- package/src/skill/skill.ts +6 -6
- package/src/storage/db.ts +1 -1
- package/src/storage/storage.ts +1 -1
- package/src/tool/bash.ts +6 -6
- package/src/tool/edit.ts +1 -1
- package/src/tool/registry.ts +2 -2
- package/src/tool/skill.ts +1 -1
- package/src/tool/task.ts +3 -3
- package/src/util/array.ts +10 -0
- package/src/util/binary.ts +41 -0
- package/src/util/encode.ts +51 -0
- package/src/util/error.ts +54 -0
- package/src/util/identifier.ts +48 -0
- package/src/util/lazy.ts +4 -16
- package/src/util/path.ts +37 -0
- package/src/util/retry.ts +41 -0
- package/src/util/slug.ts +74 -0
- package/src/worktree/index.ts +3 -3
- package/AGENTS.md +0 -10
- package/BUN_SHELL_MIGRATION_PLAN.md +0 -136
- package/Dockerfile +0 -18
- package/README.md +0 -15
- package/bunfig.toml +0 -7
- package/drizzle.config.ts +0 -10
- package/script/build.ts +0 -224
- package/script/check-migrations.ts +0 -16
- package/script/postinstall.mjs +0 -131
- package/script/publish.ts +0 -181
- package/script/schema.ts +0 -63
- package/script/seed-e2e.ts +0 -50
- package/sst-env.d.ts +0 -10
- package/test/AGENTS.md +0 -81
- package/test/acp/agent-interface.test.ts +0 -51
- package/test/acp/event-subscription.test.ts +0 -683
- package/test/agent/agent.test.ts +0 -689
- package/test/bun.test.ts +0 -53
- package/test/cli/github-action.test.ts +0 -197
- package/test/cli/github-remote.test.ts +0 -80
- package/test/cli/import.test.ts +0 -38
- package/test/cli/plugin-auth-picker.test.ts +0 -120
- package/test/cli/tui/transcript.test.ts +0 -322
- package/test/config/agent-color.test.ts +0 -71
- package/test/config/config.test.ts +0 -1886
- package/test/config/fixtures/empty-frontmatter.md +0 -4
- package/test/config/fixtures/frontmatter.md +0 -28
- package/test/config/fixtures/markdown-header.md +0 -11
- package/test/config/fixtures/no-frontmatter.md +0 -1
- package/test/config/fixtures/weird-model-id.md +0 -13
- package/test/config/markdown.test.ts +0 -228
- package/test/config/tui.test.ts +0 -510
- package/test/control-plane/session-proxy-middleware.test.ts +0 -147
- package/test/control-plane/sse.test.ts +0 -56
- package/test/control-plane/workspace-server-sse.test.ts +0 -65
- package/test/control-plane/workspace-sync.test.ts +0 -97
- package/test/file/ignore.test.ts +0 -10
- package/test/file/index.test.ts +0 -394
- package/test/file/path-traversal.test.ts +0 -198
- package/test/file/ripgrep.test.ts +0 -39
- package/test/file/time.test.ts +0 -361
- package/test/fixture/db.ts +0 -11
- package/test/fixture/fixture.ts +0 -45
- package/test/fixture/lsp/fake-lsp-server.js +0 -77
- package/test/fixture/skills/agents-sdk/SKILL.md +0 -152
- package/test/fixture/skills/agents-sdk/references/callable.md +0 -92
- package/test/fixture/skills/cloudflare/SKILL.md +0 -211
- package/test/fixture/skills/index.json +0 -6
- package/test/ide/ide.test.ts +0 -82
- package/test/keybind.test.ts +0 -421
- package/test/lsp/client.test.ts +0 -95
- package/test/mcp/headers.test.ts +0 -153
- package/test/mcp/oauth-browser.test.ts +0 -249
- package/test/memory/abort-leak.test.ts +0 -136
- package/test/patch/patch.test.ts +0 -348
- package/test/permission/arity.test.ts +0 -33
- package/test/permission/next.test.ts +0 -689
- package/test/permission-task.test.ts +0 -319
- package/test/plugin/auth-override.test.ts +0 -44
- package/test/plugin/codex.test.ts +0 -123
- package/test/preload.ts +0 -80
- package/test/project/project.test.ts +0 -348
- package/test/project/worktree-remove.test.ts +0 -65
- package/test/provider/amazon-bedrock.test.ts +0 -446
- package/test/provider/copilot/convert-to-copilot-messages.test.ts +0 -523
- package/test/provider/copilot/copilot-chat-model.test.ts +0 -592
- package/test/provider/gitlab-duo.test.ts +0 -262
- package/test/provider/provider.test.ts +0 -2220
- package/test/provider/transform.test.ts +0 -2353
- package/test/pty/pty-output-isolation.test.ts +0 -140
- package/test/question/question.test.ts +0 -300
- package/test/scheduler.test.ts +0 -73
- package/test/server/global-session-list.test.ts +0 -89
- package/test/server/session-list.test.ts +0 -90
- package/test/server/session-select.test.ts +0 -78
- package/test/session/compaction.test.ts +0 -423
- package/test/session/instruction.test.ts +0 -170
- package/test/session/llm.test.ts +0 -667
- package/test/session/message-v2.test.ts +0 -924
- package/test/session/prompt.test.ts +0 -211
- package/test/session/retry.test.ts +0 -188
- package/test/session/revert-compact.test.ts +0 -285
- package/test/session/session.test.ts +0 -71
- package/test/session/structured-output-integration.test.ts +0 -233
- package/test/session/structured-output.test.ts +0 -385
- package/test/skill/discovery.test.ts +0 -110
- package/test/skill/skill.test.ts +0 -388
- package/test/snapshot/snapshot.test.ts +0 -1180
- package/test/storage/json-migration.test.ts +0 -846
- package/test/tool/__snapshots__/tool.test.ts.snap +0 -9
- package/test/tool/apply_patch.test.ts +0 -566
- package/test/tool/bash.test.ts +0 -402
- package/test/tool/edit.test.ts +0 -496
- package/test/tool/external-directory.test.ts +0 -127
- package/test/tool/fixtures/large-image.png +0 -0
- package/test/tool/fixtures/models-api.json +0 -38413
- package/test/tool/grep.test.ts +0 -110
- package/test/tool/question.test.ts +0 -107
- package/test/tool/read.test.ts +0 -504
- package/test/tool/registry.test.ts +0 -122
- package/test/tool/skill.test.ts +0 -112
- package/test/tool/truncation.test.ts +0 -160
- package/test/tool/webfetch.test.ts +0 -100
- package/test/tool/write.test.ts +0 -348
- package/test/util/filesystem.test.ts +0 -443
- package/test/util/format.test.ts +0 -59
- package/test/util/glob.test.ts +0 -164
- package/test/util/iife.test.ts +0 -36
- package/test/util/lazy.test.ts +0 -50
- package/test/util/lock.test.ts +0 -72
- package/test/util/process.test.ts +0 -59
- package/test/util/timeout.test.ts +0 -21
- package/test/util/wildcard.test.ts +0 -90
- package/tsconfig.json +0 -16
|
@@ -1,846 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
|
2
|
-
import { Database } from "bun:sqlite"
|
|
3
|
-
import { drizzle } from "drizzle-orm/bun-sqlite"
|
|
4
|
-
import { migrate } from "drizzle-orm/bun-sqlite/migrator"
|
|
5
|
-
import path from "path"
|
|
6
|
-
import fs from "fs/promises"
|
|
7
|
-
import { readFileSync, readdirSync } from "fs"
|
|
8
|
-
import { JsonMigration } from "../../src/storage/json-migration"
|
|
9
|
-
import { Global } from "../../src/global"
|
|
10
|
-
import { ProjectTable } from "../../src/project/project.sql"
|
|
11
|
-
import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql"
|
|
12
|
-
import { SessionShareTable } from "../../src/share/share.sql"
|
|
13
|
-
|
|
14
|
-
// Test fixtures
|
|
15
|
-
const fixtures = {
|
|
16
|
-
project: {
|
|
17
|
-
id: "proj_test123abc",
|
|
18
|
-
name: "Test Project",
|
|
19
|
-
worktree: "/test/path",
|
|
20
|
-
vcs: "git" as const,
|
|
21
|
-
sandboxes: [],
|
|
22
|
-
},
|
|
23
|
-
session: {
|
|
24
|
-
id: "ses_test456def",
|
|
25
|
-
projectID: "proj_test123abc",
|
|
26
|
-
slug: "test-session",
|
|
27
|
-
directory: "/test/path",
|
|
28
|
-
title: "Test Session",
|
|
29
|
-
version: "1.0.0",
|
|
30
|
-
time: { created: 1700000000000, updated: 1700000001000 },
|
|
31
|
-
},
|
|
32
|
-
message: {
|
|
33
|
-
id: "msg_test789ghi",
|
|
34
|
-
sessionID: "ses_test456def",
|
|
35
|
-
role: "user" as const,
|
|
36
|
-
agent: "default",
|
|
37
|
-
model: { providerID: "openai", modelID: "gpt-4" },
|
|
38
|
-
time: { created: 1700000000000 },
|
|
39
|
-
},
|
|
40
|
-
part: {
|
|
41
|
-
id: "prt_testabc123",
|
|
42
|
-
messageID: "msg_test789ghi",
|
|
43
|
-
sessionID: "ses_test456def",
|
|
44
|
-
type: "text" as const,
|
|
45
|
-
text: "Hello, world!",
|
|
46
|
-
},
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Helper to create test storage directory structure
|
|
50
|
-
async function setupStorageDir() {
|
|
51
|
-
const storageDir = path.join(Global.Path.data, "storage")
|
|
52
|
-
await fs.rm(storageDir, { recursive: true, force: true })
|
|
53
|
-
await fs.mkdir(path.join(storageDir, "project"), { recursive: true })
|
|
54
|
-
await fs.mkdir(path.join(storageDir, "session", "proj_test123abc"), { recursive: true })
|
|
55
|
-
await fs.mkdir(path.join(storageDir, "message", "ses_test456def"), { recursive: true })
|
|
56
|
-
await fs.mkdir(path.join(storageDir, "part", "msg_test789ghi"), { recursive: true })
|
|
57
|
-
await fs.mkdir(path.join(storageDir, "session_diff"), { recursive: true })
|
|
58
|
-
await fs.mkdir(path.join(storageDir, "todo"), { recursive: true })
|
|
59
|
-
await fs.mkdir(path.join(storageDir, "permission"), { recursive: true })
|
|
60
|
-
await fs.mkdir(path.join(storageDir, "session_share"), { recursive: true })
|
|
61
|
-
// Create legacy marker to indicate JSON storage exists
|
|
62
|
-
await Bun.write(path.join(storageDir, "migration"), "1")
|
|
63
|
-
return storageDir
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function writeProject(storageDir: string, project: Record<string, unknown>) {
|
|
67
|
-
await Bun.write(path.join(storageDir, "project", `${project.id}.json`), JSON.stringify(project))
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function writeSession(storageDir: string, projectID: string, session: Record<string, unknown>) {
|
|
71
|
-
await Bun.write(path.join(storageDir, "session", projectID, `${session.id}.json`), JSON.stringify(session))
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Helper to create in-memory test database with schema
|
|
75
|
-
function createTestDb() {
|
|
76
|
-
const sqlite = new Database(":memory:")
|
|
77
|
-
sqlite.exec("PRAGMA foreign_keys = ON")
|
|
78
|
-
|
|
79
|
-
// Apply schema migrations using drizzle migrate
|
|
80
|
-
const dir = path.join(import.meta.dirname, "../../migration")
|
|
81
|
-
const entries = readdirSync(dir, { withFileTypes: true })
|
|
82
|
-
const migrations = entries
|
|
83
|
-
.filter((entry) => entry.isDirectory())
|
|
84
|
-
.map((entry) => ({
|
|
85
|
-
sql: readFileSync(path.join(dir, entry.name, "migration.sql"), "utf-8"),
|
|
86
|
-
timestamp: Number(entry.name.split("_")[0]),
|
|
87
|
-
}))
|
|
88
|
-
.sort((a, b) => a.timestamp - b.timestamp)
|
|
89
|
-
migrate(drizzle({ client: sqlite }), migrations)
|
|
90
|
-
|
|
91
|
-
return sqlite
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
describe("JSON to SQLite migration", () => {
|
|
95
|
-
let storageDir: string
|
|
96
|
-
let sqlite: Database
|
|
97
|
-
|
|
98
|
-
beforeEach(async () => {
|
|
99
|
-
storageDir = await setupStorageDir()
|
|
100
|
-
sqlite = createTestDb()
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
afterEach(async () => {
|
|
104
|
-
sqlite.close()
|
|
105
|
-
await fs.rm(storageDir, { recursive: true, force: true })
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
test("migrates project", async () => {
|
|
109
|
-
await writeProject(storageDir, {
|
|
110
|
-
id: "proj_test123abc",
|
|
111
|
-
worktree: "/test/path",
|
|
112
|
-
vcs: "git",
|
|
113
|
-
name: "Test Project",
|
|
114
|
-
time: { created: 1700000000000, updated: 1700000001000 },
|
|
115
|
-
sandboxes: ["/test/sandbox"],
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
const stats = await JsonMigration.run(sqlite)
|
|
119
|
-
|
|
120
|
-
expect(stats?.projects).toBe(1)
|
|
121
|
-
|
|
122
|
-
const db = drizzle({ client: sqlite })
|
|
123
|
-
const projects = db.select().from(ProjectTable).all()
|
|
124
|
-
expect(projects.length).toBe(1)
|
|
125
|
-
expect(projects[0].id).toBe("proj_test123abc")
|
|
126
|
-
expect(projects[0].worktree).toBe("/test/path")
|
|
127
|
-
expect(projects[0].name).toBe("Test Project")
|
|
128
|
-
expect(projects[0].sandboxes).toEqual(["/test/sandbox"])
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
test("uses filename for project id when JSON has different value", async () => {
|
|
132
|
-
await Bun.write(
|
|
133
|
-
path.join(storageDir, "project", "proj_filename.json"),
|
|
134
|
-
JSON.stringify({
|
|
135
|
-
id: "proj_different_in_json", // Stale! Should be ignored
|
|
136
|
-
worktree: "/test/path",
|
|
137
|
-
vcs: "git",
|
|
138
|
-
name: "Test Project",
|
|
139
|
-
sandboxes: [],
|
|
140
|
-
}),
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
const stats = await JsonMigration.run(sqlite)
|
|
144
|
-
|
|
145
|
-
expect(stats?.projects).toBe(1)
|
|
146
|
-
|
|
147
|
-
const db = drizzle({ client: sqlite })
|
|
148
|
-
const projects = db.select().from(ProjectTable).all()
|
|
149
|
-
expect(projects.length).toBe(1)
|
|
150
|
-
expect(projects[0].id).toBe("proj_filename") // Uses filename, not JSON id
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
test("migrates project with commands", async () => {
|
|
154
|
-
await writeProject(storageDir, {
|
|
155
|
-
id: "proj_with_commands",
|
|
156
|
-
worktree: "/test/path",
|
|
157
|
-
vcs: "git",
|
|
158
|
-
name: "Project With Commands",
|
|
159
|
-
time: { created: 1700000000000, updated: 1700000001000 },
|
|
160
|
-
sandboxes: ["/test/sandbox"],
|
|
161
|
-
commands: { start: "npm run dev" },
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
const stats = await JsonMigration.run(sqlite)
|
|
165
|
-
|
|
166
|
-
expect(stats?.projects).toBe(1)
|
|
167
|
-
|
|
168
|
-
const db = drizzle({ client: sqlite })
|
|
169
|
-
const projects = db.select().from(ProjectTable).all()
|
|
170
|
-
expect(projects.length).toBe(1)
|
|
171
|
-
expect(projects[0].id).toBe("proj_with_commands")
|
|
172
|
-
expect(projects[0].commands).toEqual({ start: "npm run dev" })
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
test("migrates project without commands field", async () => {
|
|
176
|
-
await writeProject(storageDir, {
|
|
177
|
-
id: "proj_no_commands",
|
|
178
|
-
worktree: "/test/path",
|
|
179
|
-
vcs: "git",
|
|
180
|
-
name: "Project Without Commands",
|
|
181
|
-
time: { created: 1700000000000, updated: 1700000001000 },
|
|
182
|
-
sandboxes: [],
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
const stats = await JsonMigration.run(sqlite)
|
|
186
|
-
|
|
187
|
-
expect(stats?.projects).toBe(1)
|
|
188
|
-
|
|
189
|
-
const db = drizzle({ client: sqlite })
|
|
190
|
-
const projects = db.select().from(ProjectTable).all()
|
|
191
|
-
expect(projects.length).toBe(1)
|
|
192
|
-
expect(projects[0].id).toBe("proj_no_commands")
|
|
193
|
-
expect(projects[0].commands).toBeNull()
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
test("migrates session with individual columns", async () => {
|
|
197
|
-
await writeProject(storageDir, {
|
|
198
|
-
id: "proj_test123abc",
|
|
199
|
-
worktree: "/test/path",
|
|
200
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
201
|
-
sandboxes: [],
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
await writeSession(storageDir, "proj_test123abc", {
|
|
205
|
-
id: "ses_test456def",
|
|
206
|
-
projectID: "proj_test123abc",
|
|
207
|
-
slug: "test-session",
|
|
208
|
-
directory: "/test/dir",
|
|
209
|
-
title: "Test Session Title",
|
|
210
|
-
version: "1.0.0",
|
|
211
|
-
time: { created: 1700000000000, updated: 1700000001000 },
|
|
212
|
-
summary: { additions: 10, deletions: 5, files: 3 },
|
|
213
|
-
share: { url: "https://example.com/share" },
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
await JsonMigration.run(sqlite)
|
|
217
|
-
|
|
218
|
-
const db = drizzle({ client: sqlite })
|
|
219
|
-
const sessions = db.select().from(SessionTable).all()
|
|
220
|
-
expect(sessions.length).toBe(1)
|
|
221
|
-
expect(sessions[0].id).toBe("ses_test456def")
|
|
222
|
-
expect(sessions[0].project_id).toBe("proj_test123abc")
|
|
223
|
-
expect(sessions[0].slug).toBe("test-session")
|
|
224
|
-
expect(sessions[0].title).toBe("Test Session Title")
|
|
225
|
-
expect(sessions[0].summary_additions).toBe(10)
|
|
226
|
-
expect(sessions[0].summary_deletions).toBe(5)
|
|
227
|
-
expect(sessions[0].share_url).toBe("https://example.com/share")
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
test("migrates messages and parts", async () => {
|
|
231
|
-
await writeProject(storageDir, {
|
|
232
|
-
id: "proj_test123abc",
|
|
233
|
-
worktree: "/",
|
|
234
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
235
|
-
sandboxes: [],
|
|
236
|
-
})
|
|
237
|
-
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
|
238
|
-
await Bun.write(
|
|
239
|
-
path.join(storageDir, "message", "ses_test456def", "msg_test789ghi.json"),
|
|
240
|
-
JSON.stringify({ ...fixtures.message }),
|
|
241
|
-
)
|
|
242
|
-
await Bun.write(
|
|
243
|
-
path.join(storageDir, "part", "msg_test789ghi", "prt_testabc123.json"),
|
|
244
|
-
JSON.stringify({ ...fixtures.part }),
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
const stats = await JsonMigration.run(sqlite)
|
|
248
|
-
|
|
249
|
-
expect(stats?.messages).toBe(1)
|
|
250
|
-
expect(stats?.parts).toBe(1)
|
|
251
|
-
|
|
252
|
-
const db = drizzle({ client: sqlite })
|
|
253
|
-
const messages = db.select().from(MessageTable).all()
|
|
254
|
-
expect(messages.length).toBe(1)
|
|
255
|
-
expect(messages[0].id).toBe("msg_test789ghi")
|
|
256
|
-
|
|
257
|
-
const parts = db.select().from(PartTable).all()
|
|
258
|
-
expect(parts.length).toBe(1)
|
|
259
|
-
expect(parts[0].id).toBe("prt_testabc123")
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
test("migrates legacy parts without ids in body", async () => {
|
|
263
|
-
await writeProject(storageDir, {
|
|
264
|
-
id: "proj_test123abc",
|
|
265
|
-
worktree: "/",
|
|
266
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
267
|
-
sandboxes: [],
|
|
268
|
-
})
|
|
269
|
-
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
|
270
|
-
await Bun.write(
|
|
271
|
-
path.join(storageDir, "message", "ses_test456def", "msg_test789ghi.json"),
|
|
272
|
-
JSON.stringify({
|
|
273
|
-
role: "user",
|
|
274
|
-
agent: "default",
|
|
275
|
-
model: { providerID: "openai", modelID: "gpt-4" },
|
|
276
|
-
time: { created: 1700000000000 },
|
|
277
|
-
}),
|
|
278
|
-
)
|
|
279
|
-
await Bun.write(
|
|
280
|
-
path.join(storageDir, "part", "msg_test789ghi", "prt_testabc123.json"),
|
|
281
|
-
JSON.stringify({
|
|
282
|
-
type: "text",
|
|
283
|
-
text: "Hello, world!",
|
|
284
|
-
}),
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
const stats = await JsonMigration.run(sqlite)
|
|
288
|
-
|
|
289
|
-
expect(stats?.messages).toBe(1)
|
|
290
|
-
expect(stats?.parts).toBe(1)
|
|
291
|
-
|
|
292
|
-
const db = drizzle({ client: sqlite })
|
|
293
|
-
const messages = db.select().from(MessageTable).all()
|
|
294
|
-
expect(messages.length).toBe(1)
|
|
295
|
-
expect(messages[0].id).toBe("msg_test789ghi")
|
|
296
|
-
expect(messages[0].session_id).toBe("ses_test456def")
|
|
297
|
-
expect(messages[0].data).not.toHaveProperty("id")
|
|
298
|
-
expect(messages[0].data).not.toHaveProperty("sessionID")
|
|
299
|
-
|
|
300
|
-
const parts = db.select().from(PartTable).all()
|
|
301
|
-
expect(parts.length).toBe(1)
|
|
302
|
-
expect(parts[0].id).toBe("prt_testabc123")
|
|
303
|
-
expect(parts[0].message_id).toBe("msg_test789ghi")
|
|
304
|
-
expect(parts[0].session_id).toBe("ses_test456def")
|
|
305
|
-
expect(parts[0].data).not.toHaveProperty("id")
|
|
306
|
-
expect(parts[0].data).not.toHaveProperty("messageID")
|
|
307
|
-
expect(parts[0].data).not.toHaveProperty("sessionID")
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
test("uses filename for message id when JSON has different value", async () => {
|
|
311
|
-
await writeProject(storageDir, {
|
|
312
|
-
id: "proj_test123abc",
|
|
313
|
-
worktree: "/",
|
|
314
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
315
|
-
sandboxes: [],
|
|
316
|
-
})
|
|
317
|
-
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
|
318
|
-
await Bun.write(
|
|
319
|
-
path.join(storageDir, "message", "ses_test456def", "msg_from_filename.json"),
|
|
320
|
-
JSON.stringify({
|
|
321
|
-
id: "msg_different_in_json", // Stale! Should be ignored
|
|
322
|
-
sessionID: "ses_test456def",
|
|
323
|
-
role: "user",
|
|
324
|
-
agent: "default",
|
|
325
|
-
time: { created: 1700000000000 },
|
|
326
|
-
}),
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
const stats = await JsonMigration.run(sqlite)
|
|
330
|
-
|
|
331
|
-
expect(stats?.messages).toBe(1)
|
|
332
|
-
|
|
333
|
-
const db = drizzle({ client: sqlite })
|
|
334
|
-
const messages = db.select().from(MessageTable).all()
|
|
335
|
-
expect(messages.length).toBe(1)
|
|
336
|
-
expect(messages[0].id).toBe("msg_from_filename") // Uses filename, not JSON id
|
|
337
|
-
expect(messages[0].session_id).toBe("ses_test456def")
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
test("uses paths for part id and messageID when JSON has different values", async () => {
|
|
341
|
-
await writeProject(storageDir, {
|
|
342
|
-
id: "proj_test123abc",
|
|
343
|
-
worktree: "/",
|
|
344
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
345
|
-
sandboxes: [],
|
|
346
|
-
})
|
|
347
|
-
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
|
348
|
-
await Bun.write(
|
|
349
|
-
path.join(storageDir, "message", "ses_test456def", "msg_realmsgid.json"),
|
|
350
|
-
JSON.stringify({
|
|
351
|
-
role: "user",
|
|
352
|
-
agent: "default",
|
|
353
|
-
time: { created: 1700000000000 },
|
|
354
|
-
}),
|
|
355
|
-
)
|
|
356
|
-
await Bun.write(
|
|
357
|
-
path.join(storageDir, "part", "msg_realmsgid", "prt_from_filename.json"),
|
|
358
|
-
JSON.stringify({
|
|
359
|
-
id: "prt_different_in_json", // Stale! Should be ignored
|
|
360
|
-
messageID: "msg_different_in_json", // Stale! Should be ignored
|
|
361
|
-
sessionID: "ses_test456def",
|
|
362
|
-
type: "text",
|
|
363
|
-
text: "Hello",
|
|
364
|
-
}),
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
const stats = await JsonMigration.run(sqlite)
|
|
368
|
-
|
|
369
|
-
expect(stats?.parts).toBe(1)
|
|
370
|
-
|
|
371
|
-
const db = drizzle({ client: sqlite })
|
|
372
|
-
const parts = db.select().from(PartTable).all()
|
|
373
|
-
expect(parts.length).toBe(1)
|
|
374
|
-
expect(parts[0].id).toBe("prt_from_filename") // Uses filename, not JSON id
|
|
375
|
-
expect(parts[0].message_id).toBe("msg_realmsgid") // Uses parent dir, not JSON messageID
|
|
376
|
-
})
|
|
377
|
-
|
|
378
|
-
test("skips orphaned sessions (no parent project)", async () => {
|
|
379
|
-
await Bun.write(
|
|
380
|
-
path.join(storageDir, "session", "proj_test123abc", "ses_orphan.json"),
|
|
381
|
-
JSON.stringify({
|
|
382
|
-
id: "ses_orphan",
|
|
383
|
-
projectID: "proj_nonexistent",
|
|
384
|
-
slug: "orphan",
|
|
385
|
-
directory: "/",
|
|
386
|
-
title: "Orphan",
|
|
387
|
-
version: "1.0.0",
|
|
388
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
389
|
-
}),
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
const stats = await JsonMigration.run(sqlite)
|
|
393
|
-
|
|
394
|
-
expect(stats?.sessions).toBe(0)
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
test("uses directory path for projectID when JSON has stale value", async () => {
|
|
398
|
-
// Simulates the scenario where earlier migration moved sessions to new
|
|
399
|
-
// git-based project directories but didn't update the projectID field
|
|
400
|
-
const gitBasedProjectID = "abc123gitcommit"
|
|
401
|
-
await writeProject(storageDir, {
|
|
402
|
-
id: gitBasedProjectID,
|
|
403
|
-
worktree: "/test/path",
|
|
404
|
-
vcs: "git",
|
|
405
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
406
|
-
sandboxes: [],
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
// Session is in the git-based directory but JSON still has old projectID
|
|
410
|
-
await writeSession(storageDir, gitBasedProjectID, {
|
|
411
|
-
id: "ses_migrated",
|
|
412
|
-
projectID: "old-project-name", // Stale! Should be ignored
|
|
413
|
-
slug: "migrated-session",
|
|
414
|
-
directory: "/test/path",
|
|
415
|
-
title: "Migrated Session",
|
|
416
|
-
version: "1.0.0",
|
|
417
|
-
time: { created: 1700000000000, updated: 1700000001000 },
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
const stats = await JsonMigration.run(sqlite)
|
|
421
|
-
|
|
422
|
-
expect(stats?.sessions).toBe(1)
|
|
423
|
-
|
|
424
|
-
const db = drizzle({ client: sqlite })
|
|
425
|
-
const sessions = db.select().from(SessionTable).all()
|
|
426
|
-
expect(sessions.length).toBe(1)
|
|
427
|
-
expect(sessions[0].id).toBe("ses_migrated")
|
|
428
|
-
expect(sessions[0].project_id).toBe(gitBasedProjectID) // Uses directory, not stale JSON
|
|
429
|
-
})
|
|
430
|
-
|
|
431
|
-
test("uses filename for session id when JSON has different value", async () => {
|
|
432
|
-
await writeProject(storageDir, {
|
|
433
|
-
id: "proj_test123abc",
|
|
434
|
-
worktree: "/test/path",
|
|
435
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
436
|
-
sandboxes: [],
|
|
437
|
-
})
|
|
438
|
-
|
|
439
|
-
await Bun.write(
|
|
440
|
-
path.join(storageDir, "session", "proj_test123abc", "ses_from_filename.json"),
|
|
441
|
-
JSON.stringify({
|
|
442
|
-
id: "ses_different_in_json", // Stale! Should be ignored
|
|
443
|
-
projectID: "proj_test123abc",
|
|
444
|
-
slug: "test-session",
|
|
445
|
-
directory: "/test/path",
|
|
446
|
-
title: "Test Session",
|
|
447
|
-
version: "1.0.0",
|
|
448
|
-
time: { created: 1700000000000, updated: 1700000001000 },
|
|
449
|
-
}),
|
|
450
|
-
)
|
|
451
|
-
|
|
452
|
-
const stats = await JsonMigration.run(sqlite)
|
|
453
|
-
|
|
454
|
-
expect(stats?.sessions).toBe(1)
|
|
455
|
-
|
|
456
|
-
const db = drizzle({ client: sqlite })
|
|
457
|
-
const sessions = db.select().from(SessionTable).all()
|
|
458
|
-
expect(sessions.length).toBe(1)
|
|
459
|
-
expect(sessions[0].id).toBe("ses_from_filename") // Uses filename, not JSON id
|
|
460
|
-
expect(sessions[0].project_id).toBe("proj_test123abc")
|
|
461
|
-
})
|
|
462
|
-
|
|
463
|
-
test("is idempotent (running twice doesn't duplicate)", async () => {
|
|
464
|
-
await writeProject(storageDir, {
|
|
465
|
-
id: "proj_test123abc",
|
|
466
|
-
worktree: "/",
|
|
467
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
468
|
-
sandboxes: [],
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
await JsonMigration.run(sqlite)
|
|
472
|
-
await JsonMigration.run(sqlite)
|
|
473
|
-
|
|
474
|
-
const db = drizzle({ client: sqlite })
|
|
475
|
-
const projects = db.select().from(ProjectTable).all()
|
|
476
|
-
expect(projects.length).toBe(1) // Still only 1 due to onConflictDoNothing
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
test("migrates todos", async () => {
|
|
480
|
-
await writeProject(storageDir, {
|
|
481
|
-
id: "proj_test123abc",
|
|
482
|
-
worktree: "/",
|
|
483
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
484
|
-
sandboxes: [],
|
|
485
|
-
})
|
|
486
|
-
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
|
487
|
-
|
|
488
|
-
// Create todo file (named by sessionID, contains array of todos)
|
|
489
|
-
await Bun.write(
|
|
490
|
-
path.join(storageDir, "todo", "ses_test456def.json"),
|
|
491
|
-
JSON.stringify([
|
|
492
|
-
{
|
|
493
|
-
id: "todo_1",
|
|
494
|
-
content: "First todo",
|
|
495
|
-
status: "pending",
|
|
496
|
-
priority: "high",
|
|
497
|
-
},
|
|
498
|
-
{
|
|
499
|
-
id: "todo_2",
|
|
500
|
-
content: "Second todo",
|
|
501
|
-
status: "completed",
|
|
502
|
-
priority: "medium",
|
|
503
|
-
},
|
|
504
|
-
]),
|
|
505
|
-
)
|
|
506
|
-
|
|
507
|
-
const stats = await JsonMigration.run(sqlite)
|
|
508
|
-
|
|
509
|
-
expect(stats?.todos).toBe(2)
|
|
510
|
-
|
|
511
|
-
const db = drizzle({ client: sqlite })
|
|
512
|
-
const todos = db.select().from(TodoTable).orderBy(TodoTable.position).all()
|
|
513
|
-
expect(todos.length).toBe(2)
|
|
514
|
-
expect(todos[0].content).toBe("First todo")
|
|
515
|
-
expect(todos[0].status).toBe("pending")
|
|
516
|
-
expect(todos[0].priority).toBe("high")
|
|
517
|
-
expect(todos[0].position).toBe(0)
|
|
518
|
-
expect(todos[1].content).toBe("Second todo")
|
|
519
|
-
expect(todos[1].position).toBe(1)
|
|
520
|
-
})
|
|
521
|
-
|
|
522
|
-
test("todos are ordered by position", async () => {
|
|
523
|
-
await writeProject(storageDir, {
|
|
524
|
-
id: "proj_test123abc",
|
|
525
|
-
worktree: "/",
|
|
526
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
527
|
-
sandboxes: [],
|
|
528
|
-
})
|
|
529
|
-
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
|
530
|
-
|
|
531
|
-
await Bun.write(
|
|
532
|
-
path.join(storageDir, "todo", "ses_test456def.json"),
|
|
533
|
-
JSON.stringify([
|
|
534
|
-
{ content: "Third", status: "pending", priority: "low" },
|
|
535
|
-
{ content: "First", status: "pending", priority: "high" },
|
|
536
|
-
{ content: "Second", status: "in_progress", priority: "medium" },
|
|
537
|
-
]),
|
|
538
|
-
)
|
|
539
|
-
|
|
540
|
-
await JsonMigration.run(sqlite)
|
|
541
|
-
|
|
542
|
-
const db = drizzle({ client: sqlite })
|
|
543
|
-
const todos = db.select().from(TodoTable).orderBy(TodoTable.position).all()
|
|
544
|
-
|
|
545
|
-
expect(todos.length).toBe(3)
|
|
546
|
-
expect(todos[0].content).toBe("Third")
|
|
547
|
-
expect(todos[0].position).toBe(0)
|
|
548
|
-
expect(todos[1].content).toBe("First")
|
|
549
|
-
expect(todos[1].position).toBe(1)
|
|
550
|
-
expect(todos[2].content).toBe("Second")
|
|
551
|
-
expect(todos[2].position).toBe(2)
|
|
552
|
-
})
|
|
553
|
-
|
|
554
|
-
test("migrates permissions", async () => {
|
|
555
|
-
await writeProject(storageDir, {
|
|
556
|
-
id: "proj_test123abc",
|
|
557
|
-
worktree: "/",
|
|
558
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
559
|
-
sandboxes: [],
|
|
560
|
-
})
|
|
561
|
-
|
|
562
|
-
// Create permission file (named by projectID, contains array of rules)
|
|
563
|
-
const permissionData = [
|
|
564
|
-
{ permission: "file.read", pattern: "/test/file1.ts", action: "allow" as const },
|
|
565
|
-
{ permission: "file.write", pattern: "/test/file2.ts", action: "ask" as const },
|
|
566
|
-
{ permission: "command.run", pattern: "npm install", action: "deny" as const },
|
|
567
|
-
]
|
|
568
|
-
await Bun.write(path.join(storageDir, "permission", "proj_test123abc.json"), JSON.stringify(permissionData))
|
|
569
|
-
|
|
570
|
-
const stats = await JsonMigration.run(sqlite)
|
|
571
|
-
|
|
572
|
-
expect(stats?.permissions).toBe(1)
|
|
573
|
-
|
|
574
|
-
const db = drizzle({ client: sqlite })
|
|
575
|
-
const permissions = db.select().from(PermissionTable).all()
|
|
576
|
-
expect(permissions.length).toBe(1)
|
|
577
|
-
expect(permissions[0].project_id).toBe("proj_test123abc")
|
|
578
|
-
expect(permissions[0].data).toEqual(permissionData)
|
|
579
|
-
})
|
|
580
|
-
|
|
581
|
-
test("migrates session shares", async () => {
|
|
582
|
-
await writeProject(storageDir, {
|
|
583
|
-
id: "proj_test123abc",
|
|
584
|
-
worktree: "/",
|
|
585
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
586
|
-
sandboxes: [],
|
|
587
|
-
})
|
|
588
|
-
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
|
589
|
-
|
|
590
|
-
// Create session share file (named by sessionID)
|
|
591
|
-
await Bun.write(
|
|
592
|
-
path.join(storageDir, "session_share", "ses_test456def.json"),
|
|
593
|
-
JSON.stringify({
|
|
594
|
-
id: "share_123",
|
|
595
|
-
secret: "supersecretkey",
|
|
596
|
-
url: "https://share.example.com/ses_test456def",
|
|
597
|
-
}),
|
|
598
|
-
)
|
|
599
|
-
|
|
600
|
-
const stats = await JsonMigration.run(sqlite)
|
|
601
|
-
|
|
602
|
-
expect(stats?.shares).toBe(1)
|
|
603
|
-
|
|
604
|
-
const db = drizzle({ client: sqlite })
|
|
605
|
-
const shares = db.select().from(SessionShareTable).all()
|
|
606
|
-
expect(shares.length).toBe(1)
|
|
607
|
-
expect(shares[0].session_id).toBe("ses_test456def")
|
|
608
|
-
expect(shares[0].id).toBe("share_123")
|
|
609
|
-
expect(shares[0].secret).toBe("supersecretkey")
|
|
610
|
-
expect(shares[0].url).toBe("https://share.example.com/ses_test456def")
|
|
611
|
-
})
|
|
612
|
-
|
|
613
|
-
test("returns empty stats when storage directory does not exist", async () => {
|
|
614
|
-
await fs.rm(storageDir, { recursive: true, force: true })
|
|
615
|
-
|
|
616
|
-
const stats = await JsonMigration.run(sqlite)
|
|
617
|
-
|
|
618
|
-
expect(stats.projects).toBe(0)
|
|
619
|
-
expect(stats.sessions).toBe(0)
|
|
620
|
-
expect(stats.messages).toBe(0)
|
|
621
|
-
expect(stats.parts).toBe(0)
|
|
622
|
-
expect(stats.todos).toBe(0)
|
|
623
|
-
expect(stats.permissions).toBe(0)
|
|
624
|
-
expect(stats.shares).toBe(0)
|
|
625
|
-
expect(stats.errors).toEqual([])
|
|
626
|
-
})
|
|
627
|
-
|
|
628
|
-
test("continues when a JSON file is unreadable and records an error", async () => {
|
|
629
|
-
await writeProject(storageDir, {
|
|
630
|
-
id: "proj_test123abc",
|
|
631
|
-
worktree: "/",
|
|
632
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
633
|
-
sandboxes: [],
|
|
634
|
-
})
|
|
635
|
-
await Bun.write(path.join(storageDir, "project", "broken.json"), "{ invalid json")
|
|
636
|
-
|
|
637
|
-
const stats = await JsonMigration.run(sqlite)
|
|
638
|
-
|
|
639
|
-
expect(stats.projects).toBe(1)
|
|
640
|
-
expect(stats.errors.some((x) => x.includes("failed to read") && x.includes("broken.json"))).toBe(true)
|
|
641
|
-
|
|
642
|
-
const db = drizzle({ client: sqlite })
|
|
643
|
-
const projects = db.select().from(ProjectTable).all()
|
|
644
|
-
expect(projects.length).toBe(1)
|
|
645
|
-
expect(projects[0].id).toBe("proj_test123abc")
|
|
646
|
-
})
|
|
647
|
-
|
|
648
|
-
test("skips invalid todo entries while preserving source positions", async () => {
|
|
649
|
-
await writeProject(storageDir, {
|
|
650
|
-
id: "proj_test123abc",
|
|
651
|
-
worktree: "/",
|
|
652
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
653
|
-
sandboxes: [],
|
|
654
|
-
})
|
|
655
|
-
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
|
656
|
-
|
|
657
|
-
await Bun.write(
|
|
658
|
-
path.join(storageDir, "todo", "ses_test456def.json"),
|
|
659
|
-
JSON.stringify([
|
|
660
|
-
{ content: "keep-0", status: "pending", priority: "high" },
|
|
661
|
-
{ content: "drop-1", priority: "low" },
|
|
662
|
-
{ content: "keep-2", status: "completed", priority: "medium" },
|
|
663
|
-
]),
|
|
664
|
-
)
|
|
665
|
-
|
|
666
|
-
const stats = await JsonMigration.run(sqlite)
|
|
667
|
-
expect(stats.todos).toBe(2)
|
|
668
|
-
|
|
669
|
-
const db = drizzle({ client: sqlite })
|
|
670
|
-
const todos = db.select().from(TodoTable).orderBy(TodoTable.position).all()
|
|
671
|
-
expect(todos.length).toBe(2)
|
|
672
|
-
expect(todos[0].content).toBe("keep-0")
|
|
673
|
-
expect(todos[0].position).toBe(0)
|
|
674
|
-
expect(todos[1].content).toBe("keep-2")
|
|
675
|
-
expect(todos[1].position).toBe(2)
|
|
676
|
-
})
|
|
677
|
-
|
|
678
|
-
test("skips orphaned todos, permissions, and shares", async () => {
|
|
679
|
-
await writeProject(storageDir, {
|
|
680
|
-
id: "proj_test123abc",
|
|
681
|
-
worktree: "/",
|
|
682
|
-
time: { created: Date.now(), updated: Date.now() },
|
|
683
|
-
sandboxes: [],
|
|
684
|
-
})
|
|
685
|
-
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
|
686
|
-
|
|
687
|
-
await Bun.write(
|
|
688
|
-
path.join(storageDir, "todo", "ses_test456def.json"),
|
|
689
|
-
JSON.stringify([{ content: "valid", status: "pending", priority: "high" }]),
|
|
690
|
-
)
|
|
691
|
-
await Bun.write(
|
|
692
|
-
path.join(storageDir, "todo", "ses_missing.json"),
|
|
693
|
-
JSON.stringify([{ content: "orphan", status: "pending", priority: "high" }]),
|
|
694
|
-
)
|
|
695
|
-
|
|
696
|
-
await Bun.write(
|
|
697
|
-
path.join(storageDir, "permission", "proj_test123abc.json"),
|
|
698
|
-
JSON.stringify([{ permission: "file.read" }]),
|
|
699
|
-
)
|
|
700
|
-
await Bun.write(
|
|
701
|
-
path.join(storageDir, "permission", "proj_missing.json"),
|
|
702
|
-
JSON.stringify([{ permission: "file.write" }]),
|
|
703
|
-
)
|
|
704
|
-
|
|
705
|
-
await Bun.write(
|
|
706
|
-
path.join(storageDir, "session_share", "ses_test456def.json"),
|
|
707
|
-
JSON.stringify({ id: "share_ok", secret: "secret", url: "https://ok.example.com" }),
|
|
708
|
-
)
|
|
709
|
-
await Bun.write(
|
|
710
|
-
path.join(storageDir, "session_share", "ses_missing.json"),
|
|
711
|
-
JSON.stringify({ id: "share_missing", secret: "secret", url: "https://missing.example.com" }),
|
|
712
|
-
)
|
|
713
|
-
|
|
714
|
-
const stats = await JsonMigration.run(sqlite)
|
|
715
|
-
|
|
716
|
-
expect(stats.todos).toBe(1)
|
|
717
|
-
expect(stats.permissions).toBe(1)
|
|
718
|
-
expect(stats.shares).toBe(1)
|
|
719
|
-
|
|
720
|
-
const db = drizzle({ client: sqlite })
|
|
721
|
-
expect(db.select().from(TodoTable).all().length).toBe(1)
|
|
722
|
-
expect(db.select().from(PermissionTable).all().length).toBe(1)
|
|
723
|
-
expect(db.select().from(SessionShareTable).all().length).toBe(1)
|
|
724
|
-
})
|
|
725
|
-
|
|
726
|
-
test("handles mixed corruption and partial validity in one migration run", async () => {
|
|
727
|
-
await writeProject(storageDir, {
|
|
728
|
-
id: "proj_test123abc",
|
|
729
|
-
worktree: "/ok",
|
|
730
|
-
time: { created: 1700000000000, updated: 1700000001000 },
|
|
731
|
-
sandboxes: [],
|
|
732
|
-
})
|
|
733
|
-
await Bun.write(
|
|
734
|
-
path.join(storageDir, "project", "proj_missing_id.json"),
|
|
735
|
-
JSON.stringify({ worktree: "/bad", sandboxes: [] }),
|
|
736
|
-
)
|
|
737
|
-
await Bun.write(path.join(storageDir, "project", "proj_broken.json"), "{ nope")
|
|
738
|
-
|
|
739
|
-
await writeSession(storageDir, "proj_test123abc", {
|
|
740
|
-
id: "ses_test456def",
|
|
741
|
-
projectID: "proj_test123abc",
|
|
742
|
-
slug: "ok",
|
|
743
|
-
directory: "/ok",
|
|
744
|
-
title: "Ok",
|
|
745
|
-
version: "1",
|
|
746
|
-
time: { created: 1700000000000, updated: 1700000001000 },
|
|
747
|
-
})
|
|
748
|
-
await Bun.write(
|
|
749
|
-
path.join(storageDir, "session", "proj_test123abc", "ses_missing_project.json"),
|
|
750
|
-
JSON.stringify({
|
|
751
|
-
id: "ses_missing_project",
|
|
752
|
-
slug: "bad",
|
|
753
|
-
directory: "/bad",
|
|
754
|
-
title: "Bad",
|
|
755
|
-
version: "1",
|
|
756
|
-
}),
|
|
757
|
-
)
|
|
758
|
-
await Bun.write(
|
|
759
|
-
path.join(storageDir, "session", "proj_test123abc", "ses_orphan.json"),
|
|
760
|
-
JSON.stringify({
|
|
761
|
-
id: "ses_orphan",
|
|
762
|
-
projectID: "proj_missing",
|
|
763
|
-
slug: "orphan",
|
|
764
|
-
directory: "/bad",
|
|
765
|
-
title: "Orphan",
|
|
766
|
-
version: "1",
|
|
767
|
-
}),
|
|
768
|
-
)
|
|
769
|
-
|
|
770
|
-
await Bun.write(
|
|
771
|
-
path.join(storageDir, "message", "ses_test456def", "msg_ok.json"),
|
|
772
|
-
JSON.stringify({ role: "user", time: { created: 1700000000000 } }),
|
|
773
|
-
)
|
|
774
|
-
await Bun.write(path.join(storageDir, "message", "ses_test456def", "msg_broken.json"), "{ nope")
|
|
775
|
-
await Bun.write(
|
|
776
|
-
path.join(storageDir, "message", "ses_missing", "msg_orphan.json"),
|
|
777
|
-
JSON.stringify({ role: "user", time: { created: 1700000000000 } }),
|
|
778
|
-
)
|
|
779
|
-
|
|
780
|
-
await Bun.write(
|
|
781
|
-
path.join(storageDir, "part", "msg_ok", "part_ok.json"),
|
|
782
|
-
JSON.stringify({ type: "text", text: "ok" }),
|
|
783
|
-
)
|
|
784
|
-
await Bun.write(
|
|
785
|
-
path.join(storageDir, "part", "msg_missing", "part_missing_message.json"),
|
|
786
|
-
JSON.stringify({ type: "text", text: "bad" }),
|
|
787
|
-
)
|
|
788
|
-
await Bun.write(path.join(storageDir, "part", "msg_ok", "part_broken.json"), "{ nope")
|
|
789
|
-
|
|
790
|
-
await Bun.write(
|
|
791
|
-
path.join(storageDir, "todo", "ses_test456def.json"),
|
|
792
|
-
JSON.stringify([
|
|
793
|
-
{ content: "ok", status: "pending", priority: "high" },
|
|
794
|
-
{ content: "skip", status: "pending" },
|
|
795
|
-
]),
|
|
796
|
-
)
|
|
797
|
-
await Bun.write(
|
|
798
|
-
path.join(storageDir, "todo", "ses_missing.json"),
|
|
799
|
-
JSON.stringify([{ content: "orphan", status: "pending", priority: "high" }]),
|
|
800
|
-
)
|
|
801
|
-
await Bun.write(path.join(storageDir, "todo", "ses_broken.json"), "{ nope")
|
|
802
|
-
|
|
803
|
-
await Bun.write(
|
|
804
|
-
path.join(storageDir, "permission", "proj_test123abc.json"),
|
|
805
|
-
JSON.stringify([{ permission: "file.read" }]),
|
|
806
|
-
)
|
|
807
|
-
await Bun.write(
|
|
808
|
-
path.join(storageDir, "permission", "proj_missing.json"),
|
|
809
|
-
JSON.stringify([{ permission: "file.write" }]),
|
|
810
|
-
)
|
|
811
|
-
await Bun.write(path.join(storageDir, "permission", "proj_broken.json"), "{ nope")
|
|
812
|
-
|
|
813
|
-
await Bun.write(
|
|
814
|
-
path.join(storageDir, "session_share", "ses_test456def.json"),
|
|
815
|
-
JSON.stringify({ id: "share_ok", secret: "secret", url: "https://ok.example.com" }),
|
|
816
|
-
)
|
|
817
|
-
await Bun.write(
|
|
818
|
-
path.join(storageDir, "session_share", "ses_missing.json"),
|
|
819
|
-
JSON.stringify({ id: "share_orphan", secret: "secret", url: "https://missing.example.com" }),
|
|
820
|
-
)
|
|
821
|
-
await Bun.write(path.join(storageDir, "session_share", "ses_broken.json"), "{ nope")
|
|
822
|
-
|
|
823
|
-
const stats = await JsonMigration.run(sqlite)
|
|
824
|
-
|
|
825
|
-
// Projects: proj_test123abc (valid), proj_missing_id (now derives id from filename)
|
|
826
|
-
// Sessions: ses_test456def (valid), ses_missing_project (now uses dir path),
|
|
827
|
-
// ses_orphan (now uses dir path, ignores stale projectID)
|
|
828
|
-
expect(stats.projects).toBe(2)
|
|
829
|
-
expect(stats.sessions).toBe(3)
|
|
830
|
-
expect(stats.messages).toBe(1)
|
|
831
|
-
expect(stats.parts).toBe(1)
|
|
832
|
-
expect(stats.todos).toBe(1)
|
|
833
|
-
expect(stats.permissions).toBe(1)
|
|
834
|
-
expect(stats.shares).toBe(1)
|
|
835
|
-
expect(stats.errors.length).toBeGreaterThanOrEqual(6)
|
|
836
|
-
|
|
837
|
-
const db = drizzle({ client: sqlite })
|
|
838
|
-
expect(db.select().from(ProjectTable).all().length).toBe(2)
|
|
839
|
-
expect(db.select().from(SessionTable).all().length).toBe(3)
|
|
840
|
-
expect(db.select().from(MessageTable).all().length).toBe(1)
|
|
841
|
-
expect(db.select().from(PartTable).all().length).toBe(1)
|
|
842
|
-
expect(db.select().from(TodoTable).all().length).toBe(1)
|
|
843
|
-
expect(db.select().from(PermissionTable).all().length).toBe(1)
|
|
844
|
-
expect(db.select().from(SessionShareTable).all().length).toBe(1)
|
|
845
|
-
})
|
|
846
|
-
})
|