@@ -417,6 +417,44 @@ describe("/mcp session restore", () => {
417417 expect ( body . result ?. tools ?. some ( ( tool ) => tool . name === "execute" ) ) . toBe ( true ) ;
418418 } , 15_000 ) ;
419419
420+ it ( "restores an initialized session after the idle alarm suspends the runtime" , async ( ) => {
421+ const orgId = nextOrgId ( ) ;
422+ const accountId = nextAccountId ( ) ;
423+ const bearer = makeTestBearer ( accountId , orgId ) ;
424+ await seedOrg ( orgId ) ;
425+
426+ const initializeResponse = await mcpPost ( {
427+ bearer,
428+ body : INITIALIZE_REQUEST ,
429+ } ) ;
430+ expect ( initializeResponse . status ) . toBe ( 200 ) ;
431+ const sessionId = initializeResponse . headers . get ( "mcp-session-id" ) ;
432+ expect ( sessionId ) . toBeTruthy ( ) ;
433+
434+ const ns = env . MCP_SESSION ;
435+ const stub = ns . get ( ns . idFromString ( sessionId ! ) ) ;
436+ await runInDurableObject ( stub , async ( instance , state ) => {
437+ doActivityState ( instance ) . lastActivityMs = 0 ;
438+ await state . storage . put ( LAST_ACTIVITY_KEY , Date . now ( ) - SESSION_TIMEOUT_MS - 1_000 ) ;
439+ await state . storage . setAlarm ( Date . now ( ) - 1 ) ;
440+ } ) ;
441+
442+ await runDurableObjectAlarm ( stub ) ;
443+
444+ const response = await mcpPost ( {
445+ bearer,
446+ sessionId,
447+ body : TOOLS_LIST_REQUEST ,
448+ } ) ;
449+ expect ( response . status ) . toBe ( 200 ) ;
450+ const body = ( await response . json ( ) ) as {
451+ readonly jsonrpc : string ;
452+ readonly result ?: { readonly tools ?: ReadonlyArray < { readonly name : string } > } ;
453+ } ;
454+ expect ( body . jsonrpc ) . toBe ( "2.0" ) ;
455+ expect ( body . result ?. tools ?. some ( ( tool ) => tool . name === "execute" ) ) . toBe ( true ) ;
456+ } , 15_000 ) ;
457+
420458 it ( "reproduces cross-account session reuse via leaked mcp-session-id" , async ( ) => {
421459 const victimOrgId = nextOrgId ( ) ;
422460 const attackerOrgId = nextOrgId ( ) ;
@@ -528,18 +566,19 @@ describe("McpSessionDO alarm lifecycle", () => {
528566 expect ( stored . alarm ) . toBeLessThanOrEqual ( Date . now ( ) + HEARTBEAT_MS + 1_000 ) ;
529567 } ) ;
530568
531- it ( "clears an expired session after a cold-started alarm" , async ( ) => {
569+ it ( "suspends an expired session after a cold-started alarm" , async ( ) => {
532570 const stub = env . MCP_SESSION . get ( env . MCP_SESSION . newUniqueId ( ) ) ;
571+ const sessionMeta = {
572+ organizationId : "org_alarm_expired" ,
573+ organizationName : "Alarm Expired" ,
574+ userId : "user_alarm_expired" ,
575+ } ;
576+ const lastActivity = Date . now ( ) - SESSION_TIMEOUT_MS - 1_000 ;
533577
534578 await runInDurableObject ( stub , async ( _instance , state ) => {
535- const now = Date . now ( ) ;
536- await state . storage . put ( SESSION_META_KEY , {
537- organizationId : "org_alarm_expired" ,
538- organizationName : "Alarm Expired" ,
539- userId : "user_alarm_expired" ,
540- } ) ;
541- await state . storage . put ( LAST_ACTIVITY_KEY , now - SESSION_TIMEOUT_MS - 1_000 ) ;
542- await state . storage . setAlarm ( now - 1 ) ;
579+ await state . storage . put ( SESSION_META_KEY , sessionMeta ) ;
580+ await state . storage . put ( LAST_ACTIVITY_KEY , lastActivity ) ;
581+ await state . storage . setAlarm ( Date . now ( ) - 1 ) ;
543582 } ) ;
544583 await runInDurableObject ( stub , ( instance ) => {
545584 doActivityState ( instance ) . lastActivityMs = 0 ;
@@ -553,8 +592,8 @@ describe("McpSessionDO alarm lifecycle", () => {
553592 alarm : await state . storage . getAlarm ( ) ,
554593 } ) ) ;
555594
556- expect ( stored . sessionMeta ) . toBeUndefined ( ) ;
557- expect ( stored . lastActivity ) . toBeUndefined ( ) ;
595+ expect ( stored . sessionMeta ) . toEqual ( sessionMeta ) ;
596+ expect ( stored . lastActivity ) . toBe ( lastActivity ) ;
558597 expect ( stored . alarm ) . toBeNull ( ) ;
559598 } ) ;
560599} ) ;
0 commit comments