Files
EvoScientist/tests/test_tool_error_handler.py
T
m4 c2743251e9 Initial commit of EvoScientist framework
Self-evolving AI scientist framework built on LangGraph/LangChain with
CLI/TUI core, FastAPI gateway, and Next.js frontend.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-07-13 08:07:45 +08:00

134 lines
4.7 KiB
Python

"""Tests for ToolErrorHandlerMiddleware and _friendly_error_summary."""
import traceback
from unittest.mock import MagicMock
import httpx
import pytest
from EvoScientist.middleware.tool_error_handler import (
ToolErrorHandlerMiddleware,
_friendly_error_summary,
)
# ---------------------------------------------------------------------------
# _friendly_error_summary — unit tests
# ---------------------------------------------------------------------------
def _make_tb(exc: BaseException) -> str:
"""Capture the traceback string for *exc* as if it were unhandled."""
try:
raise exc
except type(exc):
return traceback.format_exc()
class TestFriendlyErrorSummary:
"""Verify each pattern recognised by _friendly_error_summary."""
def test_connect_error_generic(self):
tb = _make_tb(httpx.ConnectError("Connection refused"))
result = _friendly_error_summary(tb, "my_tool")
assert "Network connection failed" in result
assert "my_tool" in result
def test_connect_error_with_api_hint(self):
tb = _make_tb(httpx.ConnectError("mineru.net refused"))
result = _friendly_error_summary(tb, "parse_documents")
assert "upstream API server" in result
assert "parse_documents" in result
def test_timeout_exception(self):
tb = _make_tb(httpx.TimeoutException("read timed out"))
result = _friendly_error_summary(tb, "slow_tool")
assert "timed out" in result
assert "slow_tool" in result
def test_mcp_tool_exception_wrapper(self):
"""Simulate the MCP adapter wrapper: 'Error calling tool 'xxx': msg'."""
tb = _make_tb(
RuntimeError("Error calling tool 'parse_documents': File too large")
)
result = _friendly_error_summary(tb, "parse_documents")
assert "File too large" in result
def test_mcp_tool_exception_empty_inner(self):
"""MCP wrapper with no inner message falls through to next pattern."""
tb = _make_tb(RuntimeError("Error calling tool 'x': "))
# Should fall through to the last-line fallback
result = _friendly_error_summary(tb, "x")
assert result # not empty
def test_generic_exception_last_line(self):
tb = _make_tb(ValueError("bad argument 'foo'"))
result = _friendly_error_summary(tb, "my_tool")
assert "bad argument" in result or "ValueError" in result
def test_empty_traceback_fallback(self):
result = _friendly_error_summary("", "mystery_tool")
assert "unexpected error" in result
assert "mystery_tool" in result
# ---------------------------------------------------------------------------
# ToolErrorHandlerMiddleware — integration tests
# ---------------------------------------------------------------------------
class TestToolErrorHandlerMiddleware:
"""Verify wrap_tool_call / awrap_tool_call convert exceptions to ToolMessage."""
def _make_request(self, tool_name: str = "test_tool") -> MagicMock:
req = MagicMock()
req.tool_call = {"name": tool_name, "id": "call_123", "args": {}}
return req
def test_sync_success_passthrough(self):
mw = ToolErrorHandlerMiddleware()
req = self._make_request()
expected = MagicMock()
result = mw.wrap_tool_call(req, handler=lambda r: expected)
assert result is expected
def test_sync_exception_returns_error_message(self):
mw = ToolErrorHandlerMiddleware()
req = self._make_request("failing_tool")
def boom(r):
raise httpx.ConnectError("nope")
result = mw.wrap_tool_call(req, handler=boom)
# result should be a ToolMessage
assert hasattr(result, "content")
assert "Network connection failed" in result.content
assert result.status == "error"
def test_async_exception_returns_error_message(self):
import asyncio
mw = ToolErrorHandlerMiddleware()
req = self._make_request("async_tool")
async def boom(r):
raise ValueError("something broke")
result = asyncio.get_event_loop().run_until_complete(
mw.awrap_tool_call(req, handler=boom)
)
assert hasattr(result, "content")
assert "something broke" in result.content
assert result.status == "error"
def test_graph_interrupt_propagates(self):
"""GraphInterrupt must NOT be caught — it should re-raise."""
from langgraph.errors import GraphInterrupt
mw = ToolErrorHandlerMiddleware()
req = self._make_request()
def interrupt(r):
raise GraphInterrupt()
with pytest.raises(GraphInterrupt):
mw.wrap_tool_call(req, handler=interrupt)