Skip to content

Commit b5c8bd3

Browse files
committed
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.
1 parent 2bab5e8 commit b5c8bd3

File tree

1 file changed

+163
-4
lines changed

1 file changed

+163
-4
lines changed

packages/opencode/test/storage/json-migration.test.ts

Lines changed: 163 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,28 @@ describe("JSON to SQLite migration", () => {
128128
expect(projects[0].sandboxes).toEqual(["/test/sandbox"])
129129
})
130130

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+
131153
test("migrates project with commands", async () => {
132154
await writeProject(storageDir, {
133155
id: "proj_with_commands",
@@ -285,6 +307,74 @@ describe("JSON to SQLite migration", () => {
285307
expect(parts[0].data).not.toHaveProperty("sessionID")
286308
})
287309

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+
288378
test("skips orphaned sessions (no parent project)", async () => {
289379
await Bun.write(
290380
path.join(storageDir, "session", "proj_test123abc", "ses_orphan.json"),
@@ -304,6 +394,72 @@ describe("JSON to SQLite migration", () => {
304394
expect(stats?.sessions).toBe(0)
305395
})
306396

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+
307463
test("is idempotent (running twice doesn't duplicate)", async () => {
308464
await writeProject(storageDir, {
309465
id: "proj_test123abc",
@@ -666,8 +822,11 @@ describe("JSON to SQLite migration", () => {
666822

667823
const stats = await JsonMigration.run(sqlite)
668824

669-
expect(stats.projects).toBe(1)
670-
expect(stats.sessions).toBe(1)
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)
671830
expect(stats.messages).toBe(1)
672831
expect(stats.parts).toBe(1)
673832
expect(stats.todos).toBe(1)
@@ -676,8 +835,8 @@ describe("JSON to SQLite migration", () => {
676835
expect(stats.errors.length).toBeGreaterThanOrEqual(6)
677836

678837
const db = drizzle({ client: sqlite })
679-
expect(db.select().from(ProjectTable).all().length).toBe(1)
680-
expect(db.select().from(SessionTable).all().length).toBe(1)
838+
expect(db.select().from(ProjectTable).all().length).toBe(2)
839+
expect(db.select().from(SessionTable).all().length).toBe(3)
681840
expect(db.select().from(MessageTable).all().length).toBe(1)
682841
expect(db.select().from(PartTable).all().length).toBe(1)
683842
expect(db.select().from(TodoTable).all().length).toBe(1)

0 commit comments

Comments
 (0)