@@ -317,10 +317,9 @@ async def mock_receive():
317317
318318@pytest .mark .anyio
319319async def test_idle_session_is_reaped ():
320- """Idle timeout sets a cancel scope deadline and reaps the session when it fires."""
321- idle_timeout = 300
320+ """After idle timeout fires, the session returns 404."""
322321 app = Server ("test-idle-reap" )
323- manager = StreamableHTTPSessionManager (app = app , session_idle_timeout = idle_timeout )
322+ manager = StreamableHTTPSessionManager (app = app , session_idle_timeout = 0.05 )
324323
325324 async with manager .run ():
326325 sent_messages : list [Message ] = []
@@ -338,7 +337,6 @@ async def mock_send(message: Message):
338337 async def mock_receive (): # pragma: no cover
339338 return {"type" : "http.request" , "body" : b"" , "more_body" : False }
340339
341- before = anyio .current_time ()
342340 await manager .handle_request (scope , mock_receive , mock_send )
343341
344342 session_id = None
@@ -352,35 +350,43 @@ async def mock_receive(): # pragma: no cover
352350 break
353351
354352 assert session_id is not None , "Session ID not found in response headers"
355- assert session_id in manager ._server_instances
356-
357- # Verify the idle deadline was set correctly
358- transport = manager ._server_instances [session_id ]
359- assert transport .idle_scope is not None
360- assert transport .idle_scope .deadline >= before + idle_timeout
361-
362- # Simulate time passing by expiring the deadline
363- transport .idle_scope .deadline = anyio .current_time ()
364-
365- with anyio .fail_after (5 ):
366- while session_id in manager ._server_instances :
367- await anyio .sleep (0 )
368-
369- assert session_id not in manager ._server_instances
370-
371- # Verify terminate() is idempotent
372- await transport .terminate ()
373- assert transport .is_terminated
374-
375-
376- @pytest .mark .parametrize (
377- "kwargs,match" ,
378- [
379- ({"session_idle_timeout" : - 1 }, "positive number" ),
380- ({"session_idle_timeout" : 0 }, "positive number" ),
381- ({"session_idle_timeout" : 30 , "stateless" : True }, "not supported in stateless" ),
382- ],
383- )
384- def test_session_idle_timeout_validation (kwargs : dict [str , Any ], match : str ):
385- with pytest .raises (ValueError , match = match ):
386- StreamableHTTPSessionManager (app = Server ("test" ), ** kwargs )
353+
354+ # Wait for the 50ms idle timeout to fire and cleanup to complete
355+ await anyio .sleep (0.5 )
356+
357+ # Verify via public API: old session ID now returns 404
358+ response_messages : list [Message ] = []
359+
360+ async def capture_send (message : Message ):
361+ response_messages .append (message )
362+
363+ scope_with_session = {
364+ "type" : "http" ,
365+ "method" : "POST" ,
366+ "path" : "/mcp" ,
367+ "headers" : [
368+ (b"content-type" , b"application/json" ),
369+ (b"mcp-session-id" , session_id .encode ()),
370+ ],
371+ }
372+
373+ await manager .handle_request (scope_with_session , mock_receive , capture_send )
374+
375+ response_start = next (
376+ (msg for msg in response_messages if msg ["type" ] == "http.response.start" ),
377+ None ,
378+ )
379+ assert response_start is not None
380+ assert response_start ["status" ] == 404
381+
382+
383+ def test_session_idle_timeout_rejects_non_positive ():
384+ with pytest .raises (ValueError , match = "positive number" ):
385+ StreamableHTTPSessionManager (app = Server ("test" ), session_idle_timeout = - 1 )
386+ with pytest .raises (ValueError , match = "positive number" ):
387+ StreamableHTTPSessionManager (app = Server ("test" ), session_idle_timeout = 0 )
388+
389+
390+ def test_session_idle_timeout_rejects_stateless ():
391+ with pytest .raises (RuntimeError , match = "not supported in stateless" ):
392+ StreamableHTTPSessionManager (app = Server ("test" ), session_idle_timeout = 30 , stateless = True )
0 commit comments