Files
anomalyco_opencode/packages/opencode/test/storage/json-migration.test.ts
T
Dax Raad bf0754caeb sync
2026-01-26 10:35:53 -05:00

254 lines
8.2 KiB
TypeScript

import { describe, test, expect, beforeEach, afterEach } from "bun:test"
import { Database } from "bun:sqlite"
import { drizzle } from "drizzle-orm/bun-sqlite"
import { migrate } from "drizzle-orm/bun-sqlite/migrator"
import { eq } from "drizzle-orm"
import path from "path"
import fs from "fs/promises"
import { readFileSync } from "fs"
import os from "os"
import { migrateFromJson } from "../../src/storage/json-migration"
import { ProjectTable } from "../../src/project/project.sql"
import { Project } from "../../src/project/project"
import {
SessionTable,
MessageTable,
PartTable,
SessionDiffTable,
TodoTable,
PermissionTable,
} from "../../src/session/session.sql"
import { SessionShareTable, ShareTable } from "../../src/share/share.sql"
// Test fixtures
const fixtures = {
project: {
id: "proj_test123abc",
name: "Test Project",
worktree: "/test/path",
vcs: "git" as const,
sandboxes: [],
},
session: {
id: "ses_test456def",
projectID: "proj_test123abc",
slug: "test-session",
directory: "/test/path",
title: "Test Session",
version: "1.0.0",
time: { created: 1700000000000, updated: 1700000001000 },
},
message: {
id: "msg_test789ghi",
sessionID: "ses_test456def",
role: "user" as const,
agent: "default",
model: { providerID: "openai", modelID: "gpt-4" },
time: { created: 1700000000000 },
},
part: {
id: "prt_testabc123",
messageID: "msg_test789ghi",
sessionID: "ses_test456def",
type: "text" as const,
text: "Hello, world!",
},
}
// Helper to create test storage directory structure
async function setupStorageDir(baseDir: string) {
const storageDir = path.join(baseDir, "storage")
await fs.mkdir(path.join(storageDir, "project"), { recursive: true })
await fs.mkdir(path.join(storageDir, "session", "proj_test123abc"), { recursive: true })
await fs.mkdir(path.join(storageDir, "message", "ses_test456def"), { recursive: true })
await fs.mkdir(path.join(storageDir, "part", "msg_test789ghi"), { recursive: true })
await fs.mkdir(path.join(storageDir, "session_diff"), { recursive: true })
await fs.mkdir(path.join(storageDir, "todo"), { recursive: true })
await fs.mkdir(path.join(storageDir, "permission"), { recursive: true })
await fs.mkdir(path.join(storageDir, "session_share"), { recursive: true })
await fs.mkdir(path.join(storageDir, "share"), { recursive: true })
// Create legacy marker to indicate JSON storage exists
await Bun.write(path.join(storageDir, "migration"), "1")
return storageDir
}
// Helper to create in-memory test database with schema
function createTestDb() {
const sqlite = new Database(":memory:")
sqlite.exec("PRAGMA foreign_keys = ON")
// Apply schema migrations using drizzle migrate
const dir = path.join(import.meta.dirname, "../../migration")
const journal = JSON.parse(readFileSync(path.join(dir, "meta/_journal.json"), "utf-8")) as {
entries: { tag: string; when: number }[]
}
const migrations = journal.entries.map((entry) => ({
sql: readFileSync(path.join(dir, `${entry.tag}.sql`), "utf-8"),
timestamp: entry.when,
}))
migrate(drizzle({ client: sqlite }), migrations)
return sqlite
}
describe("JSON to SQLite migration", () => {
let tmpDir: string
let storageDir: string
let sqlite: Database
beforeEach(async () => {
tmpDir = path.join(os.tmpdir(), "opencode-migration-test-" + Math.random().toString(36).slice(2))
await fs.mkdir(tmpDir, { recursive: true })
storageDir = await setupStorageDir(tmpDir)
sqlite = createTestDb()
})
afterEach(async () => {
sqlite.close()
await fs.rm(tmpDir, { recursive: true, force: true })
})
test("migrates project", async () => {
await Bun.write(
path.join(storageDir, "project", "proj_test123abc.json"),
JSON.stringify({
id: "proj_test123abc",
worktree: "/test/path",
vcs: "git",
name: "Test Project",
time: { created: 1700000000000, updated: 1700000001000 },
sandboxes: ["/test/sandbox"],
}),
)
const stats = await migrateFromJson(sqlite, storageDir)
expect(stats?.projects).toBe(1)
const db = drizzle({ client: sqlite })
const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1)
expect(projects[0].id).toBe("proj_test123abc")
expect(projects[0].worktree).toBe("/test/path")
expect(projects[0].name).toBe("Test Project")
expect(projects[0].sandboxes).toEqual(["/test/sandbox"])
})
test("migrates session with individual columns", async () => {
// First create the project
await Bun.write(
path.join(storageDir, "project", "proj_test123abc.json"),
JSON.stringify({
id: "proj_test123abc",
worktree: "/test/path",
time: { created: Date.now(), updated: Date.now() },
sandboxes: [],
}),
)
await Bun.write(
path.join(storageDir, "session", "proj_test123abc", "ses_test456def.json"),
JSON.stringify({
id: "ses_test456def",
projectID: "proj_test123abc",
slug: "test-session",
directory: "/test/dir",
title: "Test Session Title",
version: "1.0.0",
time: { created: 1700000000000, updated: 1700000001000 },
summary: { additions: 10, deletions: 5, files: 3 },
share: { url: "https://example.com/share" },
}),
)
await migrateFromJson(sqlite, storageDir)
const db = drizzle({ client: sqlite })
const sessions = db.select().from(SessionTable).all()
expect(sessions.length).toBe(1)
expect(sessions[0].id).toBe("ses_test456def")
expect(sessions[0].project_id).toBe("proj_test123abc")
expect(sessions[0].slug).toBe("test-session")
expect(sessions[0].title).toBe("Test Session Title")
expect(sessions[0].summary_additions).toBe(10)
expect(sessions[0].summary_deletions).toBe(5)
expect(sessions[0].share_url).toBe("https://example.com/share")
})
test("migrates messages and parts", async () => {
await Bun.write(
path.join(storageDir, "project", "proj_test123abc.json"),
JSON.stringify({
id: "proj_test123abc",
worktree: "/",
time: { created: Date.now(), updated: Date.now() },
sandboxes: [],
}),
)
await Bun.write(
path.join(storageDir, "session", "proj_test123abc", "ses_test456def.json"),
JSON.stringify({ ...fixtures.session }),
)
await Bun.write(
path.join(storageDir, "message", "ses_test456def", "msg_test789ghi.json"),
JSON.stringify({ ...fixtures.message }),
)
await Bun.write(
path.join(storageDir, "part", "msg_test789ghi", "prt_testabc123.json"),
JSON.stringify({ ...fixtures.part }),
)
const stats = await migrateFromJson(sqlite, storageDir)
expect(stats?.messages).toBe(1)
expect(stats?.parts).toBe(1)
const db = drizzle({ client: sqlite })
const messages = db.select().from(MessageTable).all()
expect(messages.length).toBe(1)
expect(messages[0].data.id).toBe("msg_test789ghi")
const parts = db.select().from(PartTable).all()
expect(parts.length).toBe(1)
expect(parts[0].data.id).toBe("prt_testabc123")
})
test("skips orphaned sessions (no parent project)", async () => {
await Bun.write(
path.join(storageDir, "session", "proj_test123abc", "ses_orphan.json"),
JSON.stringify({
id: "ses_orphan",
projectID: "proj_nonexistent",
slug: "orphan",
directory: "/",
title: "Orphan",
version: "1.0.0",
time: { created: Date.now(), updated: Date.now() },
}),
)
const stats = await migrateFromJson(sqlite, storageDir)
expect(stats?.sessions).toBe(0)
})
test("is idempotent (running twice doesn't duplicate)", async () => {
await Bun.write(
path.join(storageDir, "project", "proj_test123abc.json"),
JSON.stringify({
id: "proj_test123abc",
worktree: "/",
time: { created: Date.now(), updated: Date.now() },
sandboxes: [],
}),
)
await migrateFromJson(sqlite, storageDir)
await migrateFromJson(sqlite, storageDir)
const db = drizzle({ client: sqlite })
const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1) // Still only 1 due to onConflictDoNothing
})
})