Skip to content

Commit 5809089

Browse files
test: add case for cleanup on __aexit__
Session must close _response_streams properly when exiting async with scope. Session AS-IS fails the test since the cleanup logic is being cancelled forcefully.
1 parent a9cc822 commit 5809089

File tree

1 file changed

+71
-0
lines changed

1 file changed

+71
-0
lines changed

tests/shared/test_session.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,3 +337,74 @@ async def mock_server():
337337
await ev_closed.wait()
338338
with anyio.fail_after(1):
339339
await ev_response.wait()
340+
341+
342+
@pytest.mark.anyio
343+
async def test_session_aexit_cleanup():
344+
"""Test that the session is closing properly, cleaning up all resources."""
345+
pending_request_ids: list[int | str] = []
346+
requests_received = anyio.Event()
347+
client_session_closed = anyio.Event()
348+
349+
async with (
350+
anyio.create_task_group() as tg,
351+
create_client_server_memory_streams() as (client_streams, server_streams),
352+
):
353+
client_read, client_write = client_streams
354+
server_read, _ = server_streams
355+
356+
async def mock_server():
357+
"""Block responses to simulate a server that does not respond."""
358+
# Wait for two ping requests
359+
for _ in range(2):
360+
message = await server_read.receive()
361+
assert isinstance(message, SessionMessage)
362+
root = message.message.root
363+
assert isinstance(root, JSONRPCRequest)
364+
assert root.method == "ping"
365+
pending_request_ids.append(root.id)
366+
367+
# Signal that both requests have been received
368+
requests_received.set()
369+
370+
# Wait for the client session to be closed
371+
# This ensures the cleanup logic in finally block has time to run
372+
await client_session_closed.wait()
373+
374+
async def send_ping(session: ClientSession):
375+
# Since we are closing the session, "Connection closed" McpError is expected
376+
with pytest.raises(McpError) as e:
377+
await session.send_ping()
378+
assert "Connection closed" in str(e.value)
379+
380+
# Start the mock server in the background
381+
tg.start_soon(mock_server)
382+
383+
# Create a session and send multiple ping requests in background
384+
async with ClientSession(read_stream=client_read, write_stream=client_write) as session:
385+
# Verify initial state
386+
assert len(session._response_streams) == 0
387+
388+
# Start two ping requests in background
389+
tg.start_soon(send_ping, session)
390+
tg.start_soon(send_ping, session)
391+
392+
# Wait for both requests to be sent and received by server
393+
await requests_received.wait()
394+
await anyio.sleep(0.1) # Give time for streams to be created
395+
396+
# Verify we have 2 response streams
397+
assert len(session._response_streams) == 2
398+
399+
# We close the session by escaping the async with block
400+
client_session_closed.set()
401+
402+
# Since the sesssion has been closed, "Connection closed" McpError is expected
403+
with pytest.raises(McpError) as e:
404+
await session.send_ping()
405+
assert "Connection closed" in str(e.value)
406+
407+
# Verify all response streams have been cleaned up
408+
# (This happens when the async with block exits and __aexit__ is called)
409+
assert session is not None
410+
assert len(session._response_streams) == 0

0 commit comments

Comments
 (0)