|
| 1 | +import builtins |
| 2 | +import sys |
| 3 | +from unittest.mock import MagicMock, patch |
| 4 | + |
| 5 | +import httpx |
1 | 6 | import pytest |
2 | 7 |
|
3 | 8 | from agents import Agent |
4 | 9 | from agents.exceptions import UserError |
5 | | -from agents.mcp.server import _MCPServerWithClientSession |
| 10 | +from agents.mcp.server import MCPServerStreamableHttp, _MCPServerWithClientSession |
6 | 11 | from agents.run_context import RunContextWrapper |
7 | 12 |
|
| 13 | +# Handle Python version compatibility for ExceptionGroups |
| 14 | +if sys.version_info < (3, 11): |
| 15 | + from exceptiongroup import BaseExceptionGroup |
| 16 | +else: |
| 17 | + BaseExceptionGroup = builtins.BaseExceptionGroup |
| 18 | + |
8 | 19 |
|
9 | 20 | class CrashingClientSessionServer(_MCPServerWithClientSession): |
10 | 21 | def __init__(self): |
@@ -45,3 +56,30 @@ async def test_not_calling_connect_causes_error(): |
45 | 56 |
|
46 | 57 | with pytest.raises(UserError): |
47 | 58 | await server.call_tool("foo", {}) |
| 59 | + |
| 60 | + |
| 61 | +@pytest.mark.asyncio |
| 62 | +async def test_call_tool_nested_exception_group_mapping(): |
| 63 | + """ |
| 64 | + Regression test ensuring that nested ExceptionGroups containing HTTP errors |
| 65 | + are recursively extracted and mapped to a UserError in call_tool(). |
| 66 | + """ |
| 67 | + # 1. Initialize the server with mock streamable parameters |
| 68 | + server = MCPServerStreamableHttp(params={"url": "http://fake-mcp-server"}) |
| 69 | + |
| 70 | + # 2. Simulate an active connection by mocking the session object |
| 71 | + server.session = MagicMock() |
| 72 | + |
| 73 | + # 3. Construct a nested ExceptionGroup hierarchy containing a connection error |
| 74 | + http_error = httpx.ConnectError("Network unreachable") |
| 75 | + inner_group = BaseExceptionGroup("inner_failures", [http_error]) |
| 76 | + outer_group = BaseExceptionGroup("outer_failures", [inner_group]) |
| 77 | + |
| 78 | + # 4 & 5. Mock the internal retry handler to raise the nested group, and assert UserError |
| 79 | + with patch.object(server, "_call_tool_with_isolated_retry", side_effect=outer_group): |
| 80 | + with pytest.raises(UserError) as exc_info: |
| 81 | + await server.call_tool(tool_name="test_tool", arguments={}) |
| 82 | + |
| 83 | + # 6. Verify that the user-facing message is mapped correctly based on the root cause |
| 84 | + assert "Connection lost" in str(exc_info.value) |
| 85 | + assert exc_info.value.__cause__ is http_error |
0 commit comments