@@ -217,6 +217,51 @@ describe('getSessionsCached', () => {
217217 cwd : '/projects/bar' ,
218218 } ) ;
219219 } ) ;
220+
221+ it ( 'keeps a recent zero-turn inactive session within the grace period' , async ( ) => {
222+ const thirtyMinutesAgo = Date . now ( ) - 30 * 60 * 1000 ;
223+ mockListSessionsMeta . mockReturnValue ( [
224+ {
225+ sessionId : 'sess-recent-zero' ,
226+ summary : 'Just created' ,
227+ numTurns : 0 ,
228+ promptCount : 0 ,
229+ isActive : false ,
230+ createdAt : thirtyMinutesAgo ,
231+ updatedAt : thirtyMinutesAgo ,
232+ branch : null ,
233+ cwd : null ,
234+ } ,
235+ ] ) ;
236+
237+ const { getSessionsCached } = await import ( '../chat.js' ) ;
238+ const { sessions } = getSessionsCached ( ) ;
239+
240+ expect ( sessions ) . toHaveLength ( 1 ) ;
241+ expect ( sessions [ 0 ] . id ) . toBe ( 'sess-recent-zero' ) ;
242+ } ) ;
243+
244+ it ( 'hides an old zero-turn inactive session past the grace period' , async ( ) => {
245+ const twoHoursAgo = Date . now ( ) - 2 * 60 * 60 * 1000 ;
246+ mockListSessionsMeta . mockReturnValue ( [
247+ {
248+ sessionId : 'sess-old-zero' ,
249+ summary : 'Stale empty session' ,
250+ numTurns : 0 ,
251+ promptCount : 0 ,
252+ isActive : false ,
253+ createdAt : twoHoursAgo ,
254+ updatedAt : twoHoursAgo ,
255+ branch : null ,
256+ cwd : null ,
257+ } ,
258+ ] ) ;
259+
260+ const { getSessionsCached } = await import ( '../chat.js' ) ;
261+ const { sessions } = getSessionsCached ( ) ;
262+
263+ expect ( sessions ) . toHaveLength ( 0 ) ;
264+ } ) ;
220265} ) ;
221266
222267describe ( 'syncSessionTimestamps' , ( ) => {
@@ -347,6 +392,63 @@ describe('syncSessionTimestamps', () => {
347392 } ) ;
348393} ) ;
349394
395+ describe ( 'resume timestamp preservation' , ( ) => {
396+ it ( 'preserves updatedAt when resuming an existing session' , async ( ) => {
397+ const originalUpdatedAt = Date . now ( ) - 3600_000 ; // 1 hour ago
398+ const existingMeta = {
399+ sessionId : 'sess-resume' ,
400+ summary : 'Old session' ,
401+ updatedAt : originalUpdatedAt ,
402+ createdAt : originalUpdatedAt - 7200_000 ,
403+ numTurns : 5 ,
404+ promptCount : 3 ,
405+ isActive : false ,
406+ branch : 'main' ,
407+ cwd : '/projects/foo' ,
408+ manuallyRenamed : false ,
409+ } ;
410+ mockGetSession . mockReturnValue ( existingMeta ) ;
411+
412+ // Simulate the resume upsert pattern from startSession (chat.ts L688-701):
413+ // const existingMeta = eventStore.getSession(id);
414+ // eventStore.upsertSession({ ..., ...(existingMeta ? { updatedAt: existingMeta.updatedAt } : {}) });
415+ const sessionId = 'sess-resume' ;
416+ const retrieved = mockEventStore . getSession ( sessionId ) ;
417+ mockEventStore . upsertSession ( {
418+ sessionId,
419+ cwd : '/projects/foo' ,
420+ mode : 'code' ,
421+ branch : 'main' ,
422+ ...( retrieved ? { updatedAt : retrieved . updatedAt } : { } ) ,
423+ } ) ;
424+
425+ expect ( mockUpsertSession ) . toHaveBeenCalledWith (
426+ expect . objectContaining ( {
427+ sessionId : 'sess-resume' ,
428+ updatedAt : originalUpdatedAt ,
429+ } ) ,
430+ ) ;
431+ } ) ;
432+
433+ it ( 'omits updatedAt when resuming a session with no prior metadata' , async ( ) => {
434+ mockGetSession . mockReturnValue ( null ) ;
435+
436+ const sessionId = 'sess-new-resume' ;
437+ const retrieved = mockEventStore . getSession ( sessionId ) ;
438+ mockEventStore . upsertSession ( {
439+ sessionId,
440+ cwd : '/projects/foo' ,
441+ mode : 'code' ,
442+ branch : 'main' ,
443+ ...( retrieved ? { updatedAt : retrieved . updatedAt } : { } ) ,
444+ } ) ;
445+
446+ const call = mockUpsertSession . mock . calls [ 0 ] [ 0 ] ;
447+ expect ( call . sessionId ) . toBe ( 'sess-new-resume' ) ;
448+ expect ( call ) . not . toHaveProperty ( 'updatedAt' ) ;
449+ } ) ;
450+ } ) ;
451+
350452describe ( 'hideSession persistence' , ( ) => {
351453 it ( 'calls eventStore.hideSession to persist deletion' , async ( ) => {
352454 const { hideSession } = await import ( '../chat.js' ) ;
0 commit comments