@@ -369,6 +369,62 @@ async def drive() -> None:
369369 s2c_recv .close ()
370370
371371
372+ @pytest .mark .anyio
373+ async def test_run_closes_write_stream_after_clean_eof_without_drain_timeout ():
374+ c2s_send , c2s_recv = anyio .create_memory_object_stream [SessionMessage | Exception ](32 )
375+ s2c_send , s2c_recv = anyio .create_memory_object_stream [SessionMessage | Exception ](32 )
376+ server : JSONRPCDispatcher [TransportContext ] = JSONRPCDispatcher (
377+ c2s_recv ,
378+ s2c_send ,
379+ close_write_stream_on_read_close = False ,
380+ read_eof_drain_timeout_seconds = None ,
381+ )
382+ on_request , on_notify = echo_handlers (Recorder ())
383+
384+ with anyio .fail_after (5 ):
385+ async with anyio .create_task_group () as tg , c2s_send , c2s_recv , s2c_send , s2c_recv :
386+ await tg .start (server .run , on_request , on_notify )
387+ c2s_send .close ()
388+ with pytest .raises (anyio .EndOfStream ): # pragma: no branch
389+ await s2c_recv .receive ()
390+
391+
392+ @pytest .mark .anyio
393+ async def test_run_drains_in_flight_handlers_on_clean_eof_without_timeout ():
394+ c2s_send , c2s_recv = anyio .create_memory_object_stream [SessionMessage | Exception ](32 )
395+ s2c_send , s2c_recv = anyio .create_memory_object_stream [SessionMessage | Exception ](32 )
396+ server : JSONRPCDispatcher [TransportContext ] = JSONRPCDispatcher (
397+ c2s_recv ,
398+ s2c_send ,
399+ close_write_stream_on_read_close = False ,
400+ read_eof_drain_timeout_seconds = None ,
401+ )
402+ handler_started = anyio .Event ()
403+ handler_allowed_to_finish = anyio .Event ()
404+
405+ async def handle_request (ctx : DCtx , method : str , params : Mapping [str , Any ] | None ) -> dict [str , Any ]:
406+ handler_started .set ()
407+ await handler_allowed_to_finish .wait ()
408+ return {"drained" : True }
409+
410+ async def on_notify (ctx : DCtx , method : str , params : Mapping [str , Any ] | None ) -> None :
411+ raise NotImplementedError
412+
413+ with anyio .fail_after (5 ):
414+ async with anyio .create_task_group () as tg , c2s_send , c2s_recv , s2c_send , s2c_recv :
415+ await tg .start (server .run , handle_request , on_notify )
416+ await c2s_send .send (SessionMessage (message = JSONRPCRequest (jsonrpc = "2.0" , id = 1 , method = "x" , params = None )))
417+ await handler_started .wait ()
418+ c2s_send .close ()
419+ handler_allowed_to_finish .set ()
420+
421+ response = await s2c_recv .receive ()
422+ assert isinstance (response , SessionMessage )
423+ assert isinstance (response .message , JSONRPCResponse )
424+ assert response .message .id == 1
425+ assert response .message .result == {"drained" : True }
426+
427+
372428@pytest .mark .anyio
373429async def test_run_closes_write_stream_on_exit ():
374430 """run() owns both streams; the write end is released once the EOF teardown completes."""
0 commit comments