@@ -18,6 +18,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
1818import { scenario } from "../src/scenario" ;
1919import { Api , Mcp , Target } from "../src/services" ;
2020import type { Identity } from "../src/target" ;
21+ import { configuredMcpPausedSessionIdleTimeoutMs } from "../setup/mcp-session-timeouts" ;
2122
2223const coreApi = composePluginApi ( [ ] as const ) ;
2324
@@ -216,19 +217,19 @@ return JSON.stringify(result);
216217const executionIdOf = ( text : string ) : string | undefined =>
217218 / \b e x e c u t i o n I d : \s * ( \S + ) / . exec ( text ) ?. [ 1 ] ;
218219
219- const FAST_IDLE_TEARDOWN = process . env . E2E_MCP_FAST_IDLE_TEARDOWN === "true" ;
220+ const IDLE_TEARDOWN_BUFFER_MS = 2_000 ;
221+ const IDLE_TEARDOWN_GAP_MS = configuredMcpPausedSessionIdleTimeoutMs ( ) + IDLE_TEARDOWN_BUFFER_MS ;
222+ const IDLE_TEARDOWN_SCENARIO_TIMEOUT_MS = IDLE_TEARDOWN_GAP_MS + 120_000 ;
220223
221- // The session DO tears its runtime down after 5 minutes without a request
222- // (SESSION_TIMEOUT_MS in McpSessionDOBase) and rebuilds it from storage on
223- // the next one — the same engine-state wipe a workerd eviction or a deploy
224- // causes. Paused approvals deliberately do NOT survive this (durable pause
225- // state is out of scope); the contract is that an expired pause fails with
226- // recovery guidance and the session keeps working.
227- const IDLE_TEARDOWN_GAP = "6 minutes" ;
224+ // The session DO tears its runtime down after the configured idle ceiling and
225+ // rebuilds it from storage on the next request. The e2e harness shortens that
226+ // ceiling when it owns the cloud dev stack. Paused approvals deliberately do
227+ // not survive this (durable pause state is out of scope); the contract is that
228+ // an expired pause fails with recovery guidance and the session keeps working.
228229
229230scenario (
230231 "MCP sessions · an approval paused past the idle window expires with re-run guidance, not a dead end" ,
231- { timeout : 480_000 } ,
232+ { timeout : IDLE_TEARDOWN_SCENARIO_TIMEOUT_MS } ,
232233 Effect . gen ( function * ( ) {
233234 const target = yield * Target ;
234235 const { client } = yield * Api ;
@@ -243,7 +244,9 @@ scenario(
243244
244245 yield * Effect . gen ( function * ( ) {
245246 const first = yield * Effect . promise ( ( ) => connectClient ( target . mcpUrl , bearer ) ) ;
246- const sessionId = first . transport . sessionId ?? "" ;
247+ const sessionId = first . transport . sessionId ;
248+ expect ( sessionId , "the first client got a session id" ) . toEqual ( expect . any ( String ) ) ;
249+ if ( sessionId === undefined ) return yield * Effect . die ( "missing session id" ) ;
247250 const paused = yield * Effect . promise ( ( ) =>
248251 first . client . callTool ( { name : "execute" , arguments : { code : GATED_CODE } } ) ,
249252 ) . pipe ( Effect . ensuring ( closeQuietly ( first ) ) ) ;
@@ -253,23 +256,18 @@ scenario(
253256 ) ;
254257 const executionId = executionIdOf ( pausedText ) ;
255258 expect ( executionId , "the paused result carries the executionId" ) . toEqual ( expect . any ( String ) ) ;
259+ if ( executionId === undefined ) return yield * Effect . die ( "missing paused execution id" ) ;
256260
257261 // The user thinks the approval over for longer than the session keeps
258- // its runtime warm; the pause is gone when they come back. CI skips the
259- // real idle wait by resuming a deliberately stale id, which exercises the
260- // same recovery branch without spending six minutes in this shard.
261- const expiredExecutionId =
262- FAST_IDLE_TEARDOWN && executionId ? `${ executionId } :expired-for-ci` : executionId ;
263- if ( ! FAST_IDLE_TEARDOWN ) {
264- yield * Effect . sleep ( IDLE_TEARDOWN_GAP ) ;
265- }
262+ // its runtime warm; the pause is gone when they come back.
263+ yield * Effect . sleep ( IDLE_TEARDOWN_GAP_MS ) ;
266264
267265 const second = yield * Effect . promise ( ( ) => connectClient ( target . mcpUrl , bearer , sessionId ) ) ;
268266 yield * Effect . gen ( function * ( ) {
269267 const resumed = yield * Effect . promise ( ( ) =>
270268 second . client . callTool ( {
271269 name : "resume" ,
272- arguments : { executionId : expiredExecutionId ?? "" , action : "accept" , content : "{}" } ,
270+ arguments : { executionId, action : "accept" , content : "{}" } ,
273271 } ) ,
274272 ) ;
275273 const resumedText = textOf ( resumed ) ;
@@ -291,11 +289,12 @@ scenario(
291289 ) ;
292290 const freshExecutionId = executionIdOf ( reExecutedText ) ;
293291 expect ( freshExecutionId , "the re-run mints a different executionId" ) . not . toBe ( executionId ) ;
292+ if ( freshExecutionId === undefined ) return yield * Effect . die ( "missing fresh execution id" ) ;
294293
295294 const resumedFresh = yield * Effect . promise ( ( ) =>
296295 second . client . callTool ( {
297296 name : "resume" ,
298- arguments : { executionId : freshExecutionId ?? "" , action : "accept" , content : "{}" } ,
297+ arguments : { executionId : freshExecutionId , action : "accept" , content : "{}" } ,
299298 } ) ,
300299 ) ;
301300 expect ( resumedFresh . isError , "the fresh approval resumes to completion" ) . not . toBe ( true ) ;
0 commit comments