Files
hermes-agent/tests/tools/test_mcp_stdio_encoding_handler.py
Teknium f6938b37f3 simplify(compat): terminal/file/environments — drop 42 re-exports/aliases, repoint 20 callers + 41 test files
tools/terminal_tool.py: drop 30 pure re-export names (lifecycle/config/backends/
sudo/guards/result/interrupt/utils/_DockerEnvironment/is_managed_tool_gateway_ready)
and the noqa-F401 comments on the 25 names the facade itself uses. Sibling modules
(terminal_tool_backends/_result/_sudo/_lifecycle, environments/base, process_registry)
that read removed names through the facade now import from the defining module.
tools/environments/base.py: drop 11 re-exports (base_output/base_session_env/
path_utils) and the BaseEnvironment.stop() compat alias (no in-tree caller; the
lifecycle hasattr(env, 'stop') fallback stays for third-party envs).
tools/environments/docker.py: drop 1 re-export + the re-export comment.
Callers/tests repointed to tools.terminal_tool_{lifecycle,backends,sudo,config,
guards,result}, tools.interrupt, tools.environments.{base_output,base_session_env,
path_utils}.
2026-09-03 13:29:55 -07:00

104 lines
4.4 KiB
Python

"""Tests for MCP stdio encoding error handler fix (issue #46099).
On Windows, pipe I/O can produce non-UTF-8 bytes at chunk boundaries,
causing UnicodeDecodeError when the MCP SDK's TextReceiveStream uses
errors="strict". This test verifies that StdioServerParameters is created
with encoding_error_handler="replace".
"""
import asyncio
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from tools.mcp_tool import MCPServerTask, _MCP_AVAILABLE
pytestmark = pytest.mark.skipif(not _MCP_AVAILABLE, reason="MCP SDK not installed")
class TestStdioEncodingErrorHandler:
"""Verify that _run_stdio passes encoding_error_handler='replace'."""
def test_stdio_server_params_uses_replace_encoding_handler(self):
"""StdioServerParameters must use encoding_error_handler='replace'.
On Windows, pipe chunk boundaries can split multi-byte UTF-8 sequences,
producing bytes that fail with errors='strict'. Using 'replace' ensures
undecodable bytes become U+FFFD instead of crashing.
"""
mock_session = MagicMock()
mock_session.initialize = AsyncMock()
mock_session.list_tools = AsyncMock(return_value=SimpleNamespace(tools=[]))
mock_stdio_cm = MagicMock()
mock_stdio_cm.__aenter__ = AsyncMock(return_value=(object(), object()))
mock_stdio_cm.__aexit__ = AsyncMock(return_value=False)
mock_session_cm = MagicMock()
mock_session_cm.__aenter__ = AsyncMock(return_value=mock_session)
mock_session_cm.__aexit__ = AsyncMock(return_value=False)
async def _test():
with (
patch("tools.mcp_tool.StdioServerParameters") as mock_params,
patch("tools.mcp_tool.stdio_client", return_value=mock_stdio_cm),
patch("tools.mcp_tool.ClientSession", return_value=mock_session_cm),
patch("tools.mcp_tool_lifecycle._snapshot_child_pids", return_value=set()),
patch("tools.mcp_tool_config._write_stderr_log_header"),
patch("tools.mcp_tool._get_mcp_stderr_log", return_value=None),
):
server = MCPServerTask("test-encoding")
await server.start({
"command": "echo",
"args": ["hello"],
})
call_kwargs = mock_params.call_args.kwargs
assert call_kwargs["encoding_error_handler"] == "replace", (
f"Expected encoding_error_handler='replace', "
f"got '{call_kwargs.get('encoding_error_handler')}'"
)
await server.shutdown()
asyncio.run(_test())
def test_stdio_server_params_defaults_encoding_utf8(self):
"""Verify that the default encoding (utf-8) is not overridden."""
mock_session = MagicMock()
mock_session.initialize = AsyncMock()
mock_session.list_tools = AsyncMock(return_value=SimpleNamespace(tools=[]))
mock_stdio_cm = MagicMock()
mock_stdio_cm.__aenter__ = AsyncMock(return_value=(object(), object()))
mock_stdio_cm.__aexit__ = AsyncMock(return_value=False)
mock_session_cm = MagicMock()
mock_session_cm.__aenter__ = AsyncMock(return_value=mock_session)
mock_session_cm.__aexit__ = AsyncMock(return_value=False)
async def _test():
with (
patch("tools.mcp_tool.StdioServerParameters") as mock_params,
patch("tools.mcp_tool.stdio_client", return_value=mock_stdio_cm),
patch("tools.mcp_tool.ClientSession", return_value=mock_session_cm),
patch("tools.mcp_tool_lifecycle._snapshot_child_pids", return_value=set()),
patch("tools.mcp_tool_config._write_stderr_log_header"),
patch("tools.mcp_tool._get_mcp_stderr_log", return_value=None),
):
server = MCPServerTask("test-encoding")
await server.start({
"command": "echo",
"args": ["hello"],
})
call_kwargs = mock_params.call_args.kwargs
# encoding is not explicitly set — StdioServerParameters defaults to utf-8
# We just verify we don't override it
assert "encoding" not in call_kwargs or call_kwargs.get("encoding") == "utf-8"
await server.shutdown()
asyncio.run(_test())