@@ -249,6 +249,38 @@ function createRunningSession(text: string) {
249249 } ;
250250}
251251
252+ function createTwoTurnSession ( secondQuestion = "Follow-up" ) {
253+ return {
254+ ...createSession ( "Question" , "First answer" ) ,
255+ messages : [
256+ {
257+ id : "user-1" ,
258+ role : "user" as const ,
259+ parts : [ { type : "text" as const , text : "Question" } ] ,
260+ status : "complete" as const ,
261+ } ,
262+ {
263+ id : "assistant-1" ,
264+ role : "assistant" as const ,
265+ parts : [ { type : "text" as const , text : "First answer" } ] ,
266+ status : "complete" as const ,
267+ } ,
268+ {
269+ id : "user-2" ,
270+ role : "user" as const ,
271+ parts : [ { type : "text" as const , text : secondQuestion } ] ,
272+ status : "complete" as const ,
273+ } ,
274+ {
275+ id : "assistant-2" ,
276+ role : "assistant" as const ,
277+ parts : [ { type : "text" as const , text : "Second answer" } ] ,
278+ status : "complete" as const ,
279+ } ,
280+ ] ,
281+ } ;
282+ }
283+
252284describe ( "sessionStorage persistence layout" , ( ) => {
253285 let fakeIndexedDB : FakeIndexedDB ;
254286
@@ -275,7 +307,15 @@ describe("sessionStorage persistence layout", () => {
275307 expect ( sessionRecord . schemaVersion ) . toBe ( 4 ) ;
276308 expect ( sessionRecord . messages ) . toBeUndefined ( ) ;
277309 expect ( sessionRecord . messageRefs ) . toHaveLength ( 2 ) ;
310+ expect ( sessionRecord . messageRefs ) . toEqual ( [
311+ expect . not . objectContaining ( { signature : expect . anything ( ) } ) ,
312+ expect . not . objectContaining ( { signature : expect . anything ( ) } ) ,
313+ ] ) ;
278314 expect ( messageRecords . size ) . toBe ( 2 ) ;
315+ expect ( Array . from ( messageRecords . values ( ) ) ) . toEqual ( [
316+ expect . not . objectContaining ( { signature : expect . anything ( ) } ) ,
317+ expect . not . objectContaining ( { signature : expect . anything ( ) } ) ,
318+ ] ) ;
279319
280320 const restored = await getSession ( "session-1" ) ;
281321 expect ( restored ?. messages . map ( ( message ) => message . id ) ) . toEqual ( [
@@ -284,7 +324,7 @@ describe("sessionStorage persistence layout", () => {
284324 ] ) ;
285325 } ) ;
286326
287- it ( "only rewrites changed message rows on later saves" , async ( ) => {
327+ it ( "only rewrites the latest message row on streaming saves" , async ( ) => {
288328 const { saveSession } = await import ( "../sidepanel/sessionStorage" ) ;
289329
290330 await saveSession ( createSession ( "Question" , "First chunk" ) ) ;
@@ -305,6 +345,39 @@ describe("sessionStorage persistence layout", () => {
305345 ) . toBe ( "Second chunk" ) ;
306346 } ) ;
307347
348+ it ( "rewrites only the divergent suffix after a history edit" , async ( ) => {
349+ const { saveSession } = await import ( "../sidepanel/sessionStorage" ) ;
350+
351+ await saveSession ( createTwoTurnSession ( ) ) ;
352+
353+ const db = fakeIndexedDB . databases . get ( "huntly-agent" ) ! ;
354+ const messageStore = db . stores . get ( "session-messages" ) ! ;
355+ messageStore . putCount = 0 ;
356+
357+ const edited = createTwoTurnSession ( "Edited follow-up" ) ;
358+ edited . messages [ 2 ] = {
359+ ...edited . messages [ 2 ] ,
360+ id : "user-2-edited" ,
361+ } ;
362+ edited . messages [ 3 ] = {
363+ ...edited . messages [ 3 ] ,
364+ id : "assistant-2-edited" ,
365+ } ;
366+
367+ await saveSession ( edited ) ;
368+
369+ expect ( messageStore . putCount ) . toBe ( 2 ) ;
370+ const storedIds = Array . from ( messageStore . records . values ( ) ) . map (
371+ ( record ) => ( record as { message : { id : string } } ) . message . id
372+ ) ;
373+ expect ( storedIds ) . toEqual ( [
374+ "user-1" ,
375+ "assistant-1" ,
376+ "user-2-edited" ,
377+ "assistant-2-edited" ,
378+ ] ) ;
379+ } ) ;
380+
308381 it ( "keeps history metadata and stored messages after a run completes" , async ( ) => {
309382 const { getSession, listSessionMetadata, saveSession } = await import (
310383 "../sidepanel/sessionStorage"
@@ -328,43 +401,81 @@ describe("sessionStorage persistence layout", () => {
328401 expect ( restored ?. messages [ 1 ] . parts [ 0 ] . text ) . toBe ( "Final answer" ) ;
329402 } ) ;
330403
331- it ( "resets older chat stores before writing the current schema " , async ( ) => {
404+ it ( "migrates older chat stores without dropping persisted data " , async ( ) => {
332405 const legacyDb = new FakeDatabase ( ) ;
333- legacyDb . version = 3 ;
406+ legacyDb . version = 2 ;
334407 legacyDb . createObjectStore ( "sessions" , { keyPath : "id" } ) ;
335408 legacyDb . createObjectStore ( "session-metadata" , { keyPath : "id" } ) ;
409+ legacyDb . createObjectStore ( "session-attachments" , { keyPath : "id" } ) ;
336410 legacyDb . stores . get ( "sessions" ) ! . records . set ( "legacy-session" , {
411+ ...createSession ( "Legacy question" , "Legacy answer" ) ,
337412 id : "legacy-session" ,
338- messages : [ ] ,
413+ title : "Legacy chat" ,
414+ createdAt : "2026-04-24T08:00:00.000Z" ,
415+ updatedAt : "2026-04-24T08:00:01.000Z" ,
416+ lastMessageAt : "2026-04-24T08:00:01.000Z" ,
417+ lastOpenedAt : "2026-04-24T08:00:01.000Z" ,
339418 } ) ;
340419 legacyDb . stores . get ( "session-metadata" ) ! . records . set ( "legacy-session" , {
341420 id : "legacy-session" ,
342421 title : "Legacy chat" ,
343422 createdAt : "2026-04-24T08:00:00.000Z" ,
344- updatedAt : "2026-04-24T08:00:00 .000Z" ,
345- messageCount : 0 ,
423+ updatedAt : "2026-04-24T08:00:01 .000Z" ,
424+ messageCount : 2 ,
346425 preview : "" ,
347426 currentModelId : null ,
348427 } ) ;
428+ legacyDb . stores
429+ . get ( "session-attachments" ) !
430+ . records . set ( "legacy-attachment" , {
431+ id : "legacy-attachment" ,
432+ sessionId : "legacy-session" ,
433+ blob : { size : 17 , type : "text/plain" } as unknown as Blob ,
434+ createdAt : "2026-04-24T08:00:01.000Z" ,
435+ mediaType : "text/plain" ,
436+ size : 17 ,
437+ } ) ;
349438 fakeIndexedDB . databases . set ( "huntly-agent" , legacyDb ) ;
350439
351- const { listSessionMetadata, saveSession } = await import (
440+ const { getSession , listSessionMetadata, saveSession } = await import (
352441 "../sidepanel/sessionStorage"
353442 ) ;
354443
355- await saveSession ( createSession ( "Question" , "Answer" ) ) ;
356-
357444 const db = fakeIndexedDB . databases . get ( "huntly-agent" ) ! ;
445+ const restoredLegacy = await getSession ( "legacy-session" ) ;
446+
358447 expect ( db . version ) . toBe ( 4 ) ;
359- expect ( db . stores . get ( "sessions" ) ! . records . has ( "legacy-session" ) ) . toBe (
360- false
361- ) ;
448+ const migratedSession = db . stores
449+ . get ( "sessions" ) !
450+ . records . get ( "legacy-session" ) as Record < string , unknown > ;
451+ expect ( migratedSession . messages ) . toBeUndefined ( ) ;
452+ expect ( migratedSession . messageRefs ) . toHaveLength ( 2 ) ;
453+ expect ( db . stores . get ( "session-messages" ) ! . records . size ) . toBe ( 2 ) ;
362454 expect (
363- db . stores . get ( "session-metadata" ) ! . records . has ( "legacy-session" )
364- ) . toBe ( false ) ;
455+ db . stores . get ( "session-attachments" ) ! . records . has ( "legacy-attachment" )
456+ ) . toBe ( true ) ;
457+
458+ expect ( restoredLegacy ?. title ) . toBe ( "Legacy chat" ) ;
459+ expect ( restoredLegacy ?. messages . map ( ( message ) => message . id ) ) . toEqual ( [
460+ "user-1" ,
461+ "assistant-1" ,
462+ ] ) ;
463+ expect ( restoredLegacy ?. messages [ 1 ] . parts [ 0 ] . text ) . toBe ( "Legacy answer" ) ;
365464
366465 const metadata = await listSessionMetadata ( ) ;
367- expect ( metadata . map ( ( session ) => session . id ) ) . toEqual ( [ "session-1" ] ) ;
368- expect ( metadata [ 0 ] . preview ) . toBe ( "Question\nAnswer" ) ;
466+ expect ( metadata ) . toHaveLength ( 1 ) ;
467+ expect ( metadata [ 0 ] . id ) . toBe ( "legacy-session" ) ;
468+ expect ( metadata [ 0 ] . preview ) . toBe ( "Legacy question\nLegacy answer" ) ;
469+
470+ await saveSession ( createSession ( "Question" , "Answer" ) ) ;
471+
472+ const mergedMetadata = await listSessionMetadata ( ) ;
473+ expect ( mergedMetadata ) . toHaveLength ( 2 ) ;
474+ expect ( mergedMetadata . map ( ( session ) => session . id ) ) . toEqual (
475+ expect . arrayContaining ( [ "legacy-session" , "session-1" ] )
476+ ) ;
477+ expect (
478+ ( await getSession ( "legacy-session" ) ) ?. messages [ 1 ] . parts [ 0 ] . text
479+ ) . toBe ( "Legacy answer" ) ;
369480 } ) ;
370481} ) ;
0 commit comments