@@ -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