2025-01-23 20:10:02 +00:00
|
|
|
import anyio
|
|
|
|
|
import pytest
|
|
|
|
|
|
2026-02-03 17:42:29 +01:00
|
|
|
from mcp import Client, types
|
2025-01-23 20:10:02 +00:00
|
|
|
from mcp.client.session import ClientSession
|
2026-02-12 15:55:54 +00:00
|
|
|
from mcp.server import Server, ServerRequestContext
|
2026-01-26 14:37:44 +01:00
|
|
|
from mcp.shared.exceptions import MCPError
|
2026-01-16 15:49:26 +00:00
|
|
|
from mcp.shared.memory import create_client_server_memory_streams
|
2025-12-10 16:15:21 +00:00
|
|
|
from mcp.shared.message import SessionMessage
|
2026-02-17 10:30:34 +00:00
|
|
|
from mcp.shared.session import RequestResponder
|
2025-01-23 20:10:02 +00:00
|
|
|
from mcp.types import (
|
2026-02-17 10:30:34 +00:00
|
|
|
PARSE_ERROR,
|
2025-01-23 20:10:02 +00:00
|
|
|
CancelledNotification,
|
|
|
|
|
CancelledNotificationParams,
|
2026-02-17 10:30:34 +00:00
|
|
|
ClientResult,
|
2025-01-23 20:10:02 +00:00
|
|
|
EmptyResult,
|
2025-12-10 16:15:21 +00:00
|
|
|
ErrorData,
|
|
|
|
|
JSONRPCError,
|
|
|
|
|
JSONRPCRequest,
|
|
|
|
|
JSONRPCResponse,
|
2026-02-17 10:30:34 +00:00
|
|
|
ServerNotification,
|
|
|
|
|
ServerRequest,
|
2025-01-23 20:10:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.anyio
|
2026-01-17 08:46:58 +00:00
|
|
|
async def test_in_flight_requests_cleared_after_completion():
|
2025-01-23 20:10:02 +00:00
|
|
|
"""Verify that _in_flight is empty after all requests complete."""
|
2026-01-17 08:46:58 +00:00
|
|
|
server = Server(name="test server")
|
|
|
|
|
async with Client(server) as client:
|
|
|
|
|
# Send a request and wait for response
|
|
|
|
|
response = await client.send_ping()
|
|
|
|
|
assert isinstance(response, EmptyResult)
|
2025-01-23 20:10:02 +00:00
|
|
|
|
2026-01-17 08:46:58 +00:00
|
|
|
# Verify _in_flight is empty
|
|
|
|
|
assert len(client.session._in_flight) == 0
|
2025-01-23 20:10:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.anyio
|
|
|
|
|
async def test_request_cancellation():
|
|
|
|
|
"""Test that requests can be cancelled while in-flight."""
|
|
|
|
|
ev_tool_called = anyio.Event()
|
|
|
|
|
ev_cancelled = anyio.Event()
|
|
|
|
|
request_id = None
|
|
|
|
|
|
2026-01-17 08:46:58 +00:00
|
|
|
# Create a server with a slow tool
|
2026-02-12 15:55:54 +00:00
|
|
|
async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
|
2026-01-17 08:46:58 +00:00
|
|
|
nonlocal request_id, ev_tool_called
|
2026-02-12 15:55:54 +00:00
|
|
|
if params.name == "slow_tool":
|
|
|
|
|
request_id = ctx.request_id
|
2026-01-17 08:46:58 +00:00
|
|
|
ev_tool_called.set()
|
|
|
|
|
await anyio.sleep(10) # Long enough to ensure we can cancel
|
2026-02-12 15:55:54 +00:00
|
|
|
return types.CallToolResult(content=[]) # pragma: no cover
|
|
|
|
|
raise ValueError(f"Unknown tool: {params.name}") # pragma: no cover
|
|
|
|
|
|
|
|
|
|
async def handle_list_tools(
|
|
|
|
|
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
|
|
|
|
|
) -> types.ListToolsResult:
|
|
|
|
|
raise NotImplementedError
|
2026-01-17 08:46:58 +00:00
|
|
|
|
2026-02-12 15:55:54 +00:00
|
|
|
server = Server(
|
|
|
|
|
name="TestSessionServer",
|
|
|
|
|
on_call_tool=handle_call_tool,
|
|
|
|
|
on_list_tools=handle_list_tools,
|
|
|
|
|
)
|
2025-01-23 20:10:02 +00:00
|
|
|
|
2026-01-17 08:46:58 +00:00
|
|
|
async def make_request(client: Client):
|
2025-01-23 20:10:02 +00:00
|
|
|
nonlocal ev_cancelled
|
|
|
|
|
try:
|
2026-01-17 08:46:58 +00:00
|
|
|
await client.session.send_request(
|
2026-01-19 14:29:15 +01:00
|
|
|
types.CallToolRequest(
|
|
|
|
|
params=types.CallToolRequestParams(name="slow_tool", arguments={}),
|
2025-01-23 20:10:02 +00:00
|
|
|
),
|
|
|
|
|
types.CallToolResult,
|
|
|
|
|
)
|
|
|
|
|
pytest.fail("Request should have been cancelled") # pragma: no cover
|
2026-01-26 14:37:44 +01:00
|
|
|
except MCPError as e:
|
2025-01-23 20:10:02 +00:00
|
|
|
# Expected - request was cancelled
|
|
|
|
|
assert "Request cancelled" in str(e)
|
|
|
|
|
ev_cancelled.set()
|
|
|
|
|
|
2026-01-17 08:46:58 +00:00
|
|
|
async with Client(server) as client:
|
|
|
|
|
async with anyio.create_task_group() as tg: # pragma: no branch
|
|
|
|
|
tg.start_soon(make_request, client)
|
|
|
|
|
|
|
|
|
|
# Wait for the request to be in-flight
|
|
|
|
|
with anyio.fail_after(1): # Timeout after 1 second
|
|
|
|
|
await ev_tool_called.wait()
|
|
|
|
|
|
|
|
|
|
# Send cancellation notification
|
|
|
|
|
assert request_id is not None
|
|
|
|
|
await client.session.send_notification(
|
2026-01-19 14:29:15 +01:00
|
|
|
CancelledNotification(params=CancelledNotificationParams(request_id=request_id))
|
2026-01-17 08:46:58 +00:00
|
|
|
)
|
2025-01-23 20:10:02 +00:00
|
|
|
|
2026-01-17 08:46:58 +00:00
|
|
|
# Give cancellation time to process
|
2026-01-23 20:00:20 +00:00
|
|
|
with anyio.fail_after(1): # pragma: no branch
|
2026-01-17 08:46:58 +00:00
|
|
|
await ev_cancelled.wait()
|
2025-05-27 17:55:27 -04:00
|
|
|
|
|
|
|
|
|
2025-12-10 16:15:21 +00:00
|
|
|
@pytest.mark.anyio
|
|
|
|
|
async def test_response_id_type_mismatch_string_to_int():
|
2026-01-16 16:10:52 +00:00
|
|
|
"""Test that responses with string IDs are correctly matched to requests sent with
|
2025-12-10 16:15:21 +00:00
|
|
|
integer IDs.
|
|
|
|
|
|
|
|
|
|
This handles the case where a server returns "id": "0" (string) but the client
|
|
|
|
|
sent "id": 0 (integer). Without ID type normalization, this would cause a timeout.
|
|
|
|
|
"""
|
|
|
|
|
ev_response_received = anyio.Event()
|
|
|
|
|
result_holder: list[types.EmptyResult] = []
|
|
|
|
|
|
|
|
|
|
async with create_client_server_memory_streams() as (client_streams, server_streams):
|
|
|
|
|
client_read, client_write = client_streams
|
|
|
|
|
server_read, server_write = server_streams
|
|
|
|
|
|
|
|
|
|
async def mock_server():
|
|
|
|
|
"""Receive a request and respond with a string ID instead of integer."""
|
|
|
|
|
message = await server_read.receive()
|
|
|
|
|
assert isinstance(message, SessionMessage)
|
2026-01-19 14:04:15 +01:00
|
|
|
root = message.message
|
2025-12-10 16:15:21 +00:00
|
|
|
assert isinstance(root, JSONRPCRequest)
|
|
|
|
|
# Get the original request ID (which is an integer)
|
|
|
|
|
request_id = root.id
|
|
|
|
|
assert isinstance(request_id, int), f"Expected int, got {type(request_id)}"
|
|
|
|
|
|
|
|
|
|
# Respond with the ID as a string (simulating a buggy server)
|
|
|
|
|
response = JSONRPCResponse(
|
|
|
|
|
jsonrpc="2.0",
|
|
|
|
|
id=str(request_id), # Convert to string to simulate mismatch
|
|
|
|
|
result={},
|
|
|
|
|
)
|
2026-01-19 14:04:15 +01:00
|
|
|
await server_write.send(SessionMessage(message=response))
|
2025-12-10 16:15:21 +00:00
|
|
|
|
|
|
|
|
async def make_request(client_session: ClientSession):
|
|
|
|
|
nonlocal result_holder
|
|
|
|
|
# Send a ping request (uses integer ID internally)
|
|
|
|
|
result = await client_session.send_ping()
|
|
|
|
|
result_holder.append(result)
|
|
|
|
|
ev_response_received.set()
|
|
|
|
|
|
|
|
|
|
async with (
|
|
|
|
|
anyio.create_task_group() as tg,
|
|
|
|
|
ClientSession(read_stream=client_read, write_stream=client_write) as client_session,
|
|
|
|
|
):
|
|
|
|
|
tg.start_soon(mock_server)
|
|
|
|
|
tg.start_soon(make_request, client_session)
|
|
|
|
|
|
2026-01-23 20:00:20 +00:00
|
|
|
with anyio.fail_after(2): # pragma: no branch
|
2025-12-10 16:15:21 +00:00
|
|
|
await ev_response_received.wait()
|
|
|
|
|
|
|
|
|
|
assert len(result_holder) == 1
|
|
|
|
|
assert isinstance(result_holder[0], EmptyResult)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.anyio
|
|
|
|
|
async def test_error_response_id_type_mismatch_string_to_int():
|
2026-01-16 16:10:52 +00:00
|
|
|
"""Test that error responses with string IDs are correctly matched to requests
|
2025-12-10 16:15:21 +00:00
|
|
|
sent with integer IDs.
|
|
|
|
|
|
|
|
|
|
This handles the case where a server returns an error with "id": "0" (string)
|
|
|
|
|
but the client sent "id": 0 (integer).
|
|
|
|
|
"""
|
|
|
|
|
ev_error_received = anyio.Event()
|
2026-01-26 14:37:44 +01:00
|
|
|
error_holder: list[MCPError | Exception] = []
|
2025-12-10 16:15:21 +00:00
|
|
|
|
|
|
|
|
async with create_client_server_memory_streams() as (client_streams, server_streams):
|
|
|
|
|
client_read, client_write = client_streams
|
|
|
|
|
server_read, server_write = server_streams
|
|
|
|
|
|
|
|
|
|
async def mock_server():
|
|
|
|
|
"""Receive a request and respond with an error using a string ID."""
|
|
|
|
|
message = await server_read.receive()
|
|
|
|
|
assert isinstance(message, SessionMessage)
|
2026-01-19 14:04:15 +01:00
|
|
|
root = message.message
|
2025-12-10 16:15:21 +00:00
|
|
|
assert isinstance(root, JSONRPCRequest)
|
|
|
|
|
request_id = root.id
|
|
|
|
|
assert isinstance(request_id, int)
|
|
|
|
|
|
|
|
|
|
# Respond with an error, using the ID as a string
|
|
|
|
|
error_response = JSONRPCError(
|
|
|
|
|
jsonrpc="2.0",
|
|
|
|
|
id=str(request_id), # Convert to string to simulate mismatch
|
|
|
|
|
error=ErrorData(code=-32600, message="Test error"),
|
|
|
|
|
)
|
2026-01-19 14:04:15 +01:00
|
|
|
await server_write.send(SessionMessage(message=error_response))
|
2025-12-10 16:15:21 +00:00
|
|
|
|
|
|
|
|
async def make_request(client_session: ClientSession):
|
|
|
|
|
nonlocal error_holder
|
|
|
|
|
try:
|
|
|
|
|
await client_session.send_ping()
|
2026-01-26 14:37:44 +01:00
|
|
|
pytest.fail("Expected MCPError to be raised") # pragma: no cover
|
|
|
|
|
except MCPError as e:
|
2025-12-10 16:15:21 +00:00
|
|
|
error_holder.append(e)
|
|
|
|
|
ev_error_received.set()
|
|
|
|
|
|
|
|
|
|
async with (
|
|
|
|
|
anyio.create_task_group() as tg,
|
|
|
|
|
ClientSession(read_stream=client_read, write_stream=client_write) as client_session,
|
|
|
|
|
):
|
|
|
|
|
tg.start_soon(mock_server)
|
|
|
|
|
tg.start_soon(make_request, client_session)
|
|
|
|
|
|
2026-01-23 20:00:20 +00:00
|
|
|
with anyio.fail_after(2): # pragma: no branch
|
2025-12-10 16:15:21 +00:00
|
|
|
await ev_error_received.wait()
|
|
|
|
|
|
|
|
|
|
assert len(error_holder) == 1
|
|
|
|
|
assert "Test error" in str(error_holder[0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.anyio
|
|
|
|
|
async def test_response_id_non_numeric_string_no_match():
|
2026-01-16 16:10:52 +00:00
|
|
|
"""Test that responses with non-numeric string IDs don't incorrectly match
|
2025-12-10 16:15:21 +00:00
|
|
|
integer request IDs.
|
|
|
|
|
|
|
|
|
|
If a server returns "id": "abc" (non-numeric string), it should not match
|
|
|
|
|
a request sent with "id": 0 (integer).
|
|
|
|
|
"""
|
|
|
|
|
ev_timeout = anyio.Event()
|
|
|
|
|
|
|
|
|
|
async with create_client_server_memory_streams() as (client_streams, server_streams):
|
|
|
|
|
client_read, client_write = client_streams
|
|
|
|
|
server_read, server_write = server_streams
|
|
|
|
|
|
|
|
|
|
async def mock_server():
|
|
|
|
|
"""Receive a request and respond with a non-numeric string ID."""
|
|
|
|
|
message = await server_read.receive()
|
|
|
|
|
assert isinstance(message, SessionMessage)
|
|
|
|
|
|
|
|
|
|
# Respond with a non-numeric string ID (should not match)
|
|
|
|
|
response = JSONRPCResponse(
|
|
|
|
|
jsonrpc="2.0",
|
|
|
|
|
id="not_a_number", # Non-numeric string
|
|
|
|
|
result={},
|
|
|
|
|
)
|
2026-01-19 14:04:15 +01:00
|
|
|
await server_write.send(SessionMessage(message=response))
|
2025-12-10 16:15:21 +00:00
|
|
|
|
|
|
|
|
async def make_request(client_session: ClientSession):
|
|
|
|
|
try:
|
|
|
|
|
# Use a short timeout since we expect this to fail
|
|
|
|
|
await client_session.send_request(
|
2026-01-19 14:29:15 +01:00
|
|
|
types.PingRequest(),
|
2025-12-10 16:15:21 +00:00
|
|
|
types.EmptyResult,
|
2025-12-19 17:52:56 +05:30
|
|
|
request_read_timeout_seconds=0.5,
|
2025-12-10 16:15:21 +00:00
|
|
|
)
|
|
|
|
|
pytest.fail("Expected timeout") # pragma: no cover
|
2026-01-26 14:37:44 +01:00
|
|
|
except MCPError as e:
|
2025-12-10 16:15:21 +00:00
|
|
|
assert "Timed out" in str(e)
|
|
|
|
|
ev_timeout.set()
|
|
|
|
|
|
|
|
|
|
async with (
|
|
|
|
|
anyio.create_task_group() as tg,
|
|
|
|
|
ClientSession(read_stream=client_read, write_stream=client_write) as client_session,
|
|
|
|
|
):
|
|
|
|
|
tg.start_soon(mock_server)
|
|
|
|
|
tg.start_soon(make_request, client_session)
|
|
|
|
|
|
2026-01-23 20:00:20 +00:00
|
|
|
with anyio.fail_after(2): # pragma: no branch
|
2025-12-10 16:15:21 +00:00
|
|
|
await ev_timeout.wait()
|
|
|
|
|
|
|
|
|
|
|
2025-05-27 17:55:27 -04:00
|
|
|
@pytest.mark.anyio
|
|
|
|
|
async def test_connection_closed():
|
2026-01-16 16:10:52 +00:00
|
|
|
"""Test that pending requests are cancelled when the connection is closed remotely."""
|
2025-05-27 17:55:27 -04:00
|
|
|
|
|
|
|
|
ev_closed = anyio.Event()
|
|
|
|
|
ev_response = anyio.Event()
|
|
|
|
|
|
2025-08-11 19:56:37 +02:00
|
|
|
async with create_client_server_memory_streams() as (client_streams, server_streams):
|
2025-05-27 17:55:27 -04:00
|
|
|
client_read, client_write = client_streams
|
|
|
|
|
server_read, server_write = server_streams
|
|
|
|
|
|
2025-08-11 19:56:37 +02:00
|
|
|
async def make_request(client_session: ClientSession):
|
2025-05-27 17:55:27 -04:00
|
|
|
"""Send a request in a separate task"""
|
|
|
|
|
nonlocal ev_response
|
|
|
|
|
try:
|
|
|
|
|
# any request will do
|
|
|
|
|
await client_session.initialize()
|
|
|
|
|
pytest.fail("Request should have errored") # pragma: no cover
|
2026-01-26 14:37:44 +01:00
|
|
|
except MCPError as e:
|
2025-05-27 17:55:27 -04:00
|
|
|
# Expected - request errored
|
|
|
|
|
assert "Connection closed" in str(e)
|
|
|
|
|
ev_response.set()
|
|
|
|
|
|
|
|
|
|
async def mock_server():
|
|
|
|
|
"""Wait for a request, then close the connection"""
|
|
|
|
|
nonlocal ev_closed
|
|
|
|
|
# Wait for a request
|
|
|
|
|
await server_read.receive()
|
|
|
|
|
# Close the connection, as if the server exited
|
|
|
|
|
server_write.close()
|
|
|
|
|
server_read.close()
|
|
|
|
|
ev_closed.set()
|
|
|
|
|
|
|
|
|
|
async with (
|
|
|
|
|
anyio.create_task_group() as tg,
|
2025-08-11 19:56:37 +02:00
|
|
|
ClientSession(read_stream=client_read, write_stream=client_write) as client_session,
|
2025-05-27 17:55:27 -04:00
|
|
|
):
|
|
|
|
|
tg.start_soon(make_request, client_session)
|
|
|
|
|
tg.start_soon(mock_server)
|
|
|
|
|
|
2026-01-23 20:00:20 +00:00
|
|
|
with anyio.fail_after(1):
|
2025-05-27 17:55:27 -04:00
|
|
|
await ev_closed.wait()
|
2026-01-23 20:00:20 +00:00
|
|
|
with anyio.fail_after(1): # pragma: no branch
|
2025-05-27 17:55:27 -04:00
|
|
|
await ev_response.wait()
|
2026-02-17 10:30:34 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.anyio
|
|
|
|
|
async def test_null_id_error_surfaced_via_message_handler():
|
|
|
|
|
"""Test that a JSONRPCError with id=None is surfaced to the message handler.
|
|
|
|
|
|
|
|
|
|
Per JSON-RPC 2.0, error responses use id=null when the request id could not
|
|
|
|
|
be determined (e.g., parse errors). These cannot be correlated to any pending
|
|
|
|
|
request, so they are forwarded to the message handler as MCPError.
|
|
|
|
|
"""
|
|
|
|
|
ev_error_received = anyio.Event()
|
|
|
|
|
error_holder: list[MCPError] = []
|
|
|
|
|
|
|
|
|
|
async def capture_errors(
|
|
|
|
|
message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception,
|
|
|
|
|
) -> None:
|
|
|
|
|
assert isinstance(message, MCPError)
|
|
|
|
|
error_holder.append(message)
|
|
|
|
|
ev_error_received.set()
|
|
|
|
|
|
|
|
|
|
sent_error = ErrorData(code=PARSE_ERROR, message="Parse error")
|
|
|
|
|
|
|
|
|
|
async with create_client_server_memory_streams() as (client_streams, server_streams):
|
|
|
|
|
client_read, client_write = client_streams
|
|
|
|
|
_server_read, server_write = server_streams
|
|
|
|
|
|
|
|
|
|
async def mock_server():
|
|
|
|
|
"""Send a null-id error (simulating a parse error)."""
|
|
|
|
|
error_response = JSONRPCError(jsonrpc="2.0", id=None, error=sent_error)
|
|
|
|
|
await server_write.send(SessionMessage(message=error_response))
|
|
|
|
|
|
|
|
|
|
async with (
|
|
|
|
|
anyio.create_task_group() as tg,
|
|
|
|
|
ClientSession(
|
|
|
|
|
read_stream=client_read,
|
|
|
|
|
write_stream=client_write,
|
|
|
|
|
message_handler=capture_errors,
|
|
|
|
|
) as _client_session,
|
|
|
|
|
):
|
|
|
|
|
tg.start_soon(mock_server)
|
|
|
|
|
|
|
|
|
|
with anyio.fail_after(2): # pragma: no branch
|
|
|
|
|
await ev_error_received.wait()
|
|
|
|
|
|
|
|
|
|
assert len(error_holder) == 1
|
|
|
|
|
assert error_holder[0].error == sent_error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.anyio
|
|
|
|
|
async def test_null_id_error_does_not_affect_pending_request():
|
|
|
|
|
"""Test that a null-id error doesn't interfere with an in-flight request.
|
|
|
|
|
|
|
|
|
|
When a null-id error arrives while a request is pending, the error should
|
|
|
|
|
go to the message handler and the pending request should still complete
|
|
|
|
|
normally with its own response.
|
|
|
|
|
"""
|
|
|
|
|
ev_error_received = anyio.Event()
|
|
|
|
|
ev_response_received = anyio.Event()
|
|
|
|
|
error_holder: list[MCPError] = []
|
|
|
|
|
result_holder: list[EmptyResult] = []
|
|
|
|
|
|
|
|
|
|
async def capture_errors(
|
|
|
|
|
message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception,
|
|
|
|
|
) -> None:
|
|
|
|
|
assert isinstance(message, MCPError)
|
|
|
|
|
error_holder.append(message)
|
|
|
|
|
ev_error_received.set()
|
|
|
|
|
|
|
|
|
|
sent_error = ErrorData(code=PARSE_ERROR, message="Parse error")
|
|
|
|
|
|
|
|
|
|
async with create_client_server_memory_streams() as (client_streams, server_streams):
|
|
|
|
|
client_read, client_write = client_streams
|
|
|
|
|
server_read, server_write = server_streams
|
|
|
|
|
|
|
|
|
|
async def mock_server():
|
|
|
|
|
"""Read a request, inject a null-id error, then respond normally."""
|
|
|
|
|
message = await server_read.receive()
|
|
|
|
|
assert isinstance(message, SessionMessage)
|
|
|
|
|
assert isinstance(message.message, JSONRPCRequest)
|
|
|
|
|
request_id = message.message.id
|
|
|
|
|
|
|
|
|
|
# First, send a null-id error (should go to message handler)
|
|
|
|
|
await server_write.send(SessionMessage(message=JSONRPCError(jsonrpc="2.0", id=None, error=sent_error)))
|
|
|
|
|
|
|
|
|
|
# Then, respond normally to the pending request
|
|
|
|
|
await server_write.send(SessionMessage(message=JSONRPCResponse(jsonrpc="2.0", id=request_id, result={})))
|
|
|
|
|
|
|
|
|
|
async def make_request(client_session: ClientSession):
|
|
|
|
|
result = await client_session.send_ping()
|
|
|
|
|
result_holder.append(result)
|
|
|
|
|
ev_response_received.set()
|
|
|
|
|
|
|
|
|
|
async with (
|
|
|
|
|
anyio.create_task_group() as tg,
|
|
|
|
|
ClientSession(
|
|
|
|
|
read_stream=client_read,
|
|
|
|
|
write_stream=client_write,
|
|
|
|
|
message_handler=capture_errors,
|
|
|
|
|
) as client_session,
|
|
|
|
|
):
|
|
|
|
|
tg.start_soon(mock_server)
|
|
|
|
|
tg.start_soon(make_request, client_session)
|
|
|
|
|
|
|
|
|
|
with anyio.fail_after(2): # pragma: no branch
|
|
|
|
|
await ev_error_received.wait()
|
|
|
|
|
await ev_response_received.wait()
|
|
|
|
|
|
|
|
|
|
# Null-id error reached the message handler
|
|
|
|
|
assert len(error_holder) == 1
|
|
|
|
|
assert error_holder[0].error == sent_error
|
|
|
|
|
|
|
|
|
|
# Pending request completed successfully
|
|
|
|
|
assert len(result_holder) == 1
|
|
|
|
|
assert isinstance(result_holder[0], EmptyResult)
|