From b5c8bd3421e4b89cf9dabc6ccf019a82eefc64a5 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 14 Feb 2026 20:35:46 -0500 Subject: [PATCH] test: add tests for path-derived IDs in json migration Tests verify that file paths are used for IDs even when JSON contains different values - ensuring robustness against stale JSON content. --- .../test/storage/json-migration.test.ts | 167 +++++++++++++++++- 1 file changed, 163 insertions(+), 4 deletions(-) diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index ff05d6d05..b70c9e1eb 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -128,6 +128,28 @@ describe("JSON to SQLite migration", () => { expect(projects[0].sandboxes).toEqual(["/test/sandbox"]) }) + test("uses filename for project id when JSON has different value", async () => { + await Bun.write( + path.join(storageDir, "project", "proj_filename.json"), + JSON.stringify({ + id: "proj_different_in_json", // Stale! Should be ignored + worktree: "/test/path", + vcs: "git", + name: "Test Project", + sandboxes: [], + }), + ) + + const stats = await JsonMigration.run(sqlite) + + 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_filename") // Uses filename, not JSON id + }) + test("migrates project with commands", async () => { await writeProject(storageDir, { id: "proj_with_commands", @@ -285,6 +307,74 @@ describe("JSON to SQLite migration", () => { expect(parts[0].data).not.toHaveProperty("sessionID") }) + test("uses filename for message id when JSON has different value", async () => { + await writeProject(storageDir, { + id: "proj_test123abc", + worktree: "/", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }) + await writeSession(storageDir, "proj_test123abc", { ...fixtures.session }) + await Bun.write( + path.join(storageDir, "message", "ses_test456def", "msg_from_filename.json"), + JSON.stringify({ + id: "msg_different_in_json", // Stale! Should be ignored + sessionID: "ses_test456def", + role: "user", + agent: "default", + time: { created: 1700000000000 }, + }), + ) + + const stats = await JsonMigration.run(sqlite) + + expect(stats?.messages).toBe(1) + + const db = drizzle({ client: sqlite }) + const messages = db.select().from(MessageTable).all() + expect(messages.length).toBe(1) + expect(messages[0].id).toBe("msg_from_filename") // Uses filename, not JSON id + expect(messages[0].session_id).toBe("ses_test456def") + }) + + test("uses paths for part id and messageID when JSON has different values", async () => { + await writeProject(storageDir, { + id: "proj_test123abc", + worktree: "/", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }) + await writeSession(storageDir, "proj_test123abc", { ...fixtures.session }) + await Bun.write( + path.join(storageDir, "message", "ses_test456def", "msg_realmsgid.json"), + JSON.stringify({ + role: "user", + agent: "default", + time: { created: 1700000000000 }, + }), + ) + await Bun.write( + path.join(storageDir, "part", "msg_realmsgid", "prt_from_filename.json"), + JSON.stringify({ + id: "prt_different_in_json", // Stale! Should be ignored + messageID: "msg_different_in_json", // Stale! Should be ignored + sessionID: "ses_test456def", + type: "text", + text: "Hello", + }), + ) + + const stats = await JsonMigration.run(sqlite) + + expect(stats?.parts).toBe(1) + + const db = drizzle({ client: sqlite }) + const parts = db.select().from(PartTable).all() + expect(parts.length).toBe(1) + expect(parts[0].id).toBe("prt_from_filename") // Uses filename, not JSON id + expect(parts[0].message_id).toBe("msg_realmsgid") // Uses parent dir, not JSON messageID + }) + test("skips orphaned sessions (no parent project)", async () => { await Bun.write( path.join(storageDir, "session", "proj_test123abc", "ses_orphan.json"), @@ -304,6 +394,72 @@ describe("JSON to SQLite migration", () => { expect(stats?.sessions).toBe(0) }) + test("uses directory path for projectID when JSON has stale value", async () => { + // Simulates the scenario where earlier migration moved sessions to new + // git-based project directories but didn't update the projectID field + const gitBasedProjectID = "abc123gitcommit" + await writeProject(storageDir, { + id: gitBasedProjectID, + worktree: "/test/path", + vcs: "git", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }) + + // Session is in the git-based directory but JSON still has old projectID + await writeSession(storageDir, gitBasedProjectID, { + id: "ses_migrated", + projectID: "old-project-name", // Stale! Should be ignored + slug: "migrated-session", + directory: "/test/path", + title: "Migrated Session", + version: "1.0.0", + time: { created: 1700000000000, updated: 1700000001000 }, + }) + + const stats = await JsonMigration.run(sqlite) + + expect(stats?.sessions).toBe(1) + + const db = drizzle({ client: sqlite }) + const sessions = db.select().from(SessionTable).all() + expect(sessions.length).toBe(1) + expect(sessions[0].id).toBe("ses_migrated") + expect(sessions[0].project_id).toBe(gitBasedProjectID) // Uses directory, not stale JSON + }) + + test("uses filename for session id when JSON has different value", async () => { + await writeProject(storageDir, { + id: "proj_test123abc", + worktree: "/test/path", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }) + + await Bun.write( + path.join(storageDir, "session", "proj_test123abc", "ses_from_filename.json"), + JSON.stringify({ + id: "ses_different_in_json", // Stale! Should be ignored + projectID: "proj_test123abc", + slug: "test-session", + directory: "/test/path", + title: "Test Session", + version: "1.0.0", + time: { created: 1700000000000, updated: 1700000001000 }, + }), + ) + + const stats = await JsonMigration.run(sqlite) + + expect(stats?.sessions).toBe(1) + + const db = drizzle({ client: sqlite }) + const sessions = db.select().from(SessionTable).all() + expect(sessions.length).toBe(1) + expect(sessions[0].id).toBe("ses_from_filename") // Uses filename, not JSON id + expect(sessions[0].project_id).toBe("proj_test123abc") + }) + test("is idempotent (running twice doesn't duplicate)", async () => { await writeProject(storageDir, { id: "proj_test123abc", @@ -666,8 +822,11 @@ describe("JSON to SQLite migration", () => { const stats = await JsonMigration.run(sqlite) - expect(stats.projects).toBe(1) - expect(stats.sessions).toBe(1) + // Projects: proj_test123abc (valid), proj_missing_id (now derives id from filename) + // Sessions: ses_test456def (valid), ses_missing_project (now uses dir path), + // ses_orphan (now uses dir path, ignores stale projectID) + expect(stats.projects).toBe(2) + expect(stats.sessions).toBe(3) expect(stats.messages).toBe(1) expect(stats.parts).toBe(1) expect(stats.todos).toBe(1) @@ -676,8 +835,8 @@ describe("JSON to SQLite migration", () => { expect(stats.errors.length).toBeGreaterThanOrEqual(6) const db = drizzle({ client: sqlite }) - expect(db.select().from(ProjectTable).all().length).toBe(1) - expect(db.select().from(SessionTable).all().length).toBe(1) + expect(db.select().from(ProjectTable).all().length).toBe(2) + expect(db.select().from(SessionTable).all().length).toBe(3) expect(db.select().from(MessageTable).all().length).toBe(1) expect(db.select().from(PartTable).all().length).toBe(1) expect(db.select().from(TodoTable).all().length).toBe(1)