mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
221 lines
7.1 KiB
Python
221 lines
7.1 KiB
Python
"""Tests for MCP error logging functionality."""
|
|
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from hanzo_mcp.tools.common.error_logger import (
|
|
MCPErrorLogger,
|
|
get_error_logger,
|
|
log_call_signature_error,
|
|
log_tool_error,
|
|
)
|
|
|
|
|
|
class TestMCPErrorLogger:
|
|
"""Test suite for MCPErrorLogger."""
|
|
|
|
def test_logger_initialization(self, tmp_path):
|
|
"""Test that logger initializes correctly."""
|
|
logger = MCPErrorLogger(log_dir=tmp_path)
|
|
|
|
assert logger.log_dir == tmp_path
|
|
assert logger.log_dir.exists()
|
|
|
|
# Check that log files are created with today's date
|
|
today = datetime.now().strftime("%Y-%m-%d")
|
|
assert logger.log_file == tmp_path / f"mcp-errors-{today}.log"
|
|
assert logger.general_log_file == tmp_path / "errors.log"
|
|
|
|
def test_log_tool_error(self, tmp_path):
|
|
"""Test logging a tool error."""
|
|
logger = MCPErrorLogger(log_dir=tmp_path)
|
|
|
|
# Create a test error
|
|
test_error = ValueError("Test error message")
|
|
params = {"file_path": "/test/path", "limit": 100}
|
|
|
|
# Log the error
|
|
logger.log_tool_error(
|
|
tool_name="read",
|
|
error=test_error,
|
|
params=params,
|
|
context="Testing error logging",
|
|
)
|
|
|
|
# Check that log files were created
|
|
tool_log = tmp_path / "read-errors.log"
|
|
assert tool_log.exists()
|
|
|
|
# Read the log and verify content
|
|
content = tool_log.read_text()
|
|
assert "Test error message" in content
|
|
assert "ValueError" in content
|
|
assert "Testing error logging" in content
|
|
|
|
# Check JSON log
|
|
today = datetime.now().strftime("%Y-%m-%d")
|
|
json_log = tmp_path / f"tool-errors-{today}.jsonl"
|
|
assert json_log.exists()
|
|
|
|
def test_log_call_signature_error(self, tmp_path):
|
|
"""Test logging a call signature error."""
|
|
logger = MCPErrorLogger(log_dir=tmp_path)
|
|
|
|
# Create a test error
|
|
test_error = TypeError("takes 1 positional argument but 2 were given")
|
|
|
|
# Log the error
|
|
logger.log_call_signature_error(
|
|
tool_name="read",
|
|
expected_signature="read(ctx, file_path: str)",
|
|
actual_call="read(ctx, '/path/to/file')",
|
|
error=test_error,
|
|
)
|
|
|
|
# Check that signature errors log was created
|
|
sig_log = tmp_path / "signature-errors.log"
|
|
assert sig_log.exists()
|
|
|
|
# Read the log and verify content
|
|
content = sig_log.read_text()
|
|
assert "CALL SIGNATURE ERROR" in content
|
|
assert "read(ctx, file_path: str)" in content
|
|
assert "read(ctx, '/path/to/file')" in content
|
|
|
|
def test_sanitize_params(self, tmp_path):
|
|
"""Test that sensitive parameters are sanitized."""
|
|
logger = MCPErrorLogger(log_dir=tmp_path)
|
|
|
|
# Create params with sensitive data
|
|
params = {
|
|
"file_path": "/test/path",
|
|
"api_key": "secret-key-123",
|
|
"password": "super-secret",
|
|
"token": "bearer-token",
|
|
"normal_param": "normal-value",
|
|
}
|
|
|
|
sanitized = logger._sanitize_params(params)
|
|
|
|
# Check that sensitive keys are redacted
|
|
assert sanitized["api_key"] == "[REDACTED]"
|
|
assert sanitized["password"] == "[REDACTED]"
|
|
assert sanitized["token"] == "[REDACTED]"
|
|
|
|
# Check that normal params are preserved
|
|
assert sanitized["file_path"] == "/test/path"
|
|
assert sanitized["normal_param"] == "normal-value"
|
|
|
|
def test_get_recent_errors(self, tmp_path):
|
|
"""Test retrieving recent errors."""
|
|
logger = MCPErrorLogger(log_dir=tmp_path)
|
|
|
|
# Log multiple errors
|
|
for i in range(5):
|
|
error = ValueError(f"Error {i}")
|
|
logger.log_tool_error(
|
|
tool_name=f"tool_{i}", error=error, params={"index": i}
|
|
)
|
|
|
|
# Get recent errors
|
|
recent = logger.get_recent_errors(limit=3)
|
|
assert len(recent) == 3
|
|
|
|
# Check they're the most recent ones
|
|
assert "Error 4" in recent[-1]["error_message"]
|
|
assert "Error 3" in recent[-2]["error_message"]
|
|
|
|
def test_global_logger(self):
|
|
"""Test global logger singleton."""
|
|
logger1 = get_error_logger()
|
|
logger2 = get_error_logger()
|
|
|
|
# Should be the same instance
|
|
assert logger1 is logger2
|
|
|
|
def test_convenience_functions(self, tmp_path):
|
|
"""Test convenience logging functions."""
|
|
# Override global logger for testing
|
|
from hanzo_mcp.tools.common import error_logger as el_module
|
|
|
|
el_module._global_error_logger = MCPErrorLogger(log_dir=tmp_path)
|
|
|
|
# Test log_tool_error
|
|
error = RuntimeError("Test runtime error")
|
|
log_tool_error("test_tool", error, params={"test": "param"})
|
|
|
|
tool_log = tmp_path / "test_tool-errors.log"
|
|
assert tool_log.exists()
|
|
assert "Test runtime error" in tool_log.read_text()
|
|
|
|
# Test log_call_signature_error
|
|
sig_error = TypeError("signature mismatch")
|
|
log_call_signature_error(
|
|
"test_tool", "expected signature", "actual call", sig_error
|
|
)
|
|
|
|
sig_log = tmp_path / "signature-errors.log"
|
|
assert sig_log.exists()
|
|
|
|
|
|
class TestErrorLoggingIntegration:
|
|
"""Integration tests for error logging with tools."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_tool_with_error_logging(self, tmp_path):
|
|
"""Test that ReadTool logs errors correctly."""
|
|
from hanzo_tools.filesystem.read import ReadTool
|
|
|
|
from hanzo_mcp.tools.common import error_logger as el_module
|
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
|
|
# Override global logger
|
|
el_module._global_error_logger = MCPErrorLogger(log_dir=tmp_path)
|
|
|
|
# Create tool
|
|
perm_mgr = PermissionManager()
|
|
tool = ReadTool(perm_mgr)
|
|
|
|
# Create mock context
|
|
class MockContext:
|
|
async def info(self, msg):
|
|
pass
|
|
|
|
async def error(self, msg):
|
|
pass
|
|
|
|
async def warning(self, msg):
|
|
pass
|
|
|
|
ctx = MockContext()
|
|
|
|
# Try to read non-existent file (should log error if call signature is wrong)
|
|
# This simulates the original error where LLM called with positional args
|
|
try:
|
|
# This would cause an error if called incorrectly
|
|
result = await tool.call(ctx, file_path="/nonexistent/file.txt")
|
|
# Should return error message, not raise
|
|
assert "Error" in result or "does not exist" in result
|
|
except TypeError as e:
|
|
# If there's a type error, it should be logged
|
|
log_tool_error("read", e, context="Integration test")
|
|
|
|
# Verify error was logged
|
|
read_log = tmp_path / "read-errors.log"
|
|
assert read_log.exists()
|
|
|
|
|
|
def test_error_logger_creates_directory():
|
|
"""Test that error logger creates ~/.hanzo/mcp/logs/ directory."""
|
|
logger = get_error_logger()
|
|
|
|
expected_dir = Path.home() / ".hanzo" / "mcp" / "logs"
|
|
assert logger.log_dir == expected_dir
|
|
assert expected_dir.exists()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|