hermes-agent/tests/tools/test_mcp_empty_error_message.py
liuhao1024 f9b4b8af34 fix(mcp): include exception type in error messages when str(exc) is empty
Some exception classes (e.g. anyio.ClosedResourceError) are raised without
a message argument, so str(exc) returns an empty string. The existing error
format f'{type(exc).__name__}: {exc}' would produce messages like
'MCP call failed: ClosedResourceError: ' with nothing after the colon.

Add _exc_str() helper that falls back to repr(exc) when str(exc) is empty,
and apply it to all 6 MCP error formatting sites (5 tool/prompt/resource
handlers + 1 sampling handler).

Fixes #19417
2026-05-07 06:33:57 -07:00

89 lines
2.9 KiB
Python

"""Regression tests for MCP error messages when str(exc) is empty.
Issue #19417: ClosedResourceError (and similar exceptions raised without a
message argument) produced ``MCP call failed: ClosedResourceError: `` with
nothing after the colon, making debugging impossible.
Fix: ``_exc_str()`` falls back to ``repr(exc)`` when ``str(exc)`` is empty.
"""
import json
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import pytest
from tools.mcp_tool import _exc_str, _sanitize_error
# ---------------------------------------------------------------------------
# _exc_str unit tests
# ---------------------------------------------------------------------------
class _EmptyMessageError(Exception):
"""Exception whose __str__ returns empty string (like anyio.ClosedResourceError)."""
def __str__(self):
return ""
class _NormalError(Exception):
pass
def test_exc_str_returns_str_when_nonempty():
exc = _NormalError("something broke")
assert _exc_str(exc) == "something broke"
def test_exc_str_falls_back_to_repr_when_str_empty():
exc = _EmptyMessageError()
result = _exc_str(exc)
assert result != ""
assert "_EmptyMessageError" in result
def test_exc_str_falls_back_to_repr_for_whitespace_only():
"""str(exc) that is only whitespace should also trigger the repr fallback."""
exc = Exception(" ")
result = _exc_str(exc)
# After strip(), the text is empty, so repr is used
assert result.strip() != ""
def test_exc_str_handles_closedresource_like_exception():
"""Simulate anyio.ClosedResourceError which has no message."""
# Replicate the real anyio.ClosedResourceError behavior
exc = type("ClosedResourceError", (Exception,), {"__str__": lambda self: ""})()
result = _exc_str(exc)
assert "ClosedResourceError" in result
assert result != ""
# ---------------------------------------------------------------------------
# Integration: error message format in _sanitize_error
# ---------------------------------------------------------------------------
def test_error_message_not_empty_when_exc_has_no_message():
"""The formatted error string should always contain the exception class name."""
exc = _EmptyMessageError()
error_msg = _sanitize_error(
f"MCP call failed: {type(exc).__name__}: {_exc_str(exc)}"
)
assert "ClosedResourceError" not in error_msg or "_EmptyMessageError" in error_msg
# The key invariant: the message must not end with ": "
assert not error_msg.endswith(": ")
# And it must contain the exception type name
assert "_EmptyMessageError" in error_msg
def test_error_message_preserves_normal_exception_text():
"""Normal exceptions should still show their message text."""
exc = _NormalError("connection refused")
error_msg = _sanitize_error(
f"MCP call failed: {type(exc).__name__}: {_exc_str(exc)}"
)
assert "connection refused" in error_msg
assert "_NormalError" in error_msg