"""Tests for server error handling."""
import logging
from collections.abc import Generator
from pathlib import Path
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from mcp_server_neurolorap.server import run_dev_mode
# Disable logging for tests
logging.getLogger("mcp_server_neurolorap.server").setLevel(logging.CRITICAL)
class MockTerminal:
"""Mock terminal for testing."""
def __init__(self) -> None:
"""Initialize mock terminal."""
self.parse_request_calls = 0
self.handle_command_calls = 0
def parse_request(self, line: str) -> dict[str, Any] | None:
"""Mock parse_request method."""
self.parse_request_calls += 1
if line == "unknown_command":
return {
"jsonrpc": "2.0",
"method": "unknown_command",
"params": [],
"id": 1,
}
elif line == "exit":
return {
"jsonrpc": "2.0",
"method": "exit",
"params": [],
"id": 2,
}
return None
async def handle_command(self, request: dict[str, Any]) -> dict[str, Any]:
"""Mock handle_command method."""
self.handle_command_calls += 1
if request["method"] == "unknown_command":
return {
"error": {"message": "Method 'unknown_command' not found"},
"id": request["id"],
}
elif request["method"] == "exit":
return {"result": "Goodbye!", "id": request["id"]}
return {"error": {"message": "Invalid request"}, "id": request["id"]}
class ToolMock(AsyncMock):
"""Custom AsyncMock that matches the expected tool callable type."""
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self._side_effect: Exception | None = None
def set_side_effect(self, effect: Exception | None) -> None:
"""Set side effect for the mock."""
self._side_effect = effect
async def __call__(self, *args: Any, **kwargs: Any) -> str:
"""Mock tool call."""
try:
if self._side_effect:
raise self._side_effect
return "Success"
except Exception:
if kwargs.get("tool_name") == "project_structure_reporter":
return "Error generating report"
return "No files found to process or error occurred"
@pytest.fixture
def mock_fastmcp() -> Generator[MagicMock, None, None]:
"""Mock FastMCP server."""
with patch("mcp_server_neurolorap.server.FastMCP") as mock:
mock_server = MagicMock()
mock_server.name = "neurolorap"
mock_server.tools = {
"project_structure_reporter": ToolMock(),
"code_collector": ToolMock(),
}
mock_server.tool_called = False
mock.return_value = mock_server
yield mock_server
@pytest.mark.asyncio
async def test_project_structure_reporter_error_handling(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mock_fastmcp: MagicMock
) -> None:
"""Test error handling in project_structure_reporter tool."""
monkeypatch.setenv("MCP_PROJECT_ROOT", str(tmp_path))
# Test with invalid ignore patterns
tool = mock_fastmcp.tools["project_structure_reporter"]
tool.set_side_effect(ValueError("Invalid pattern"))
result = await tool(
tool_name="project_structure_reporter",
output_filename="test.md",
ignore_patterns=["["],
)
assert "Error generating report" in result
# Test with file system error
tool = mock_fastmcp.tools["project_structure_reporter"]
tool.set_side_effect(OSError("Permission denied"))
result = await tool(tool_name="project_structure_reporter")
assert "Error generating report" in result
# Test with analysis error
tool = mock_fastmcp.tools["project_structure_reporter"]
tool.set_side_effect(ValueError("Analysis failed"))
result = await tool(tool_name="project_structure_reporter")
assert "Error generating report" in result
# Test with report generation error
tool = mock_fastmcp.tools["project_structure_reporter"]
tool.set_side_effect(ValueError("Report generation failed"))
result = await tool(tool_name="project_structure_reporter")
assert "Error generating report" in result
@pytest.mark.asyncio
async def test_code_collector_error_handling(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mock_fastmcp: MagicMock
) -> None:
"""Test error handling in code_collector tool."""
monkeypatch.setenv("MCP_PROJECT_ROOT", str(tmp_path))
# Test with invalid input path
tool = mock_fastmcp.tools["code_collector"]
tool.set_side_effect(ValueError("Invalid path"))
result = await tool(
tool_name="code_collector",
input_path="/nonexistent/path",
title="Test",
)
assert "No files found to process or error occurred" in result
# Test with file system error
tool = mock_fastmcp.tools["code_collector"]
tool.set_side_effect(OSError("Permission denied"))
result = await tool(tool_name="code_collector")
assert "No files found to process or error occurred" in result
# Test with collection error
tool = mock_fastmcp.tools["code_collector"]
tool.set_side_effect(ValueError("Collection failed"))
result = await tool(tool_name="code_collector")
assert "No files found to process or error occurred" in result
# Test with unexpected error
tool = mock_fastmcp.tools["code_collector"]
tool.set_side_effect(Exception("Unexpected error"))
result = await tool(tool_name="code_collector")
assert "No files found to process or error occurred" in result
@pytest.mark.asyncio
async def test_run_dev_mode_value_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test error handling in run_dev_mode."""
monkeypatch.setenv("MCP_PROJECT_ROOT", "/tmp")
# Mock print function to capture output
prints: list[str] = []
def print_mock(x: object) -> None:
prints.append(str(x))
monkeypatch.setattr("builtins.print", print_mock)
# Mock input to raise ValueError
input_mock = MagicMock(side_effect=[ValueError("Invalid input"), "exit"])
monkeypatch.setattr("builtins.input", input_mock)
# Run dev mode
await run_dev_mode()
# Verify error handling
assert any("Value error: Invalid input" in msg for msg in prints)
assert any("Exiting developer mode" in msg for msg in prints)
@pytest.mark.asyncio
async def test_run_dev_mode_type_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test type error handling in run_dev_mode."""
monkeypatch.setenv("MCP_PROJECT_ROOT", "/tmp")
# Mock print function to capture output
prints: list[str] = []
def print_mock(x: object) -> None:
prints.append(str(x))
monkeypatch.setattr("builtins.print", print_mock)
# Mock input to raise TypeError
input_mock = MagicMock(side_effect=[TypeError("Invalid type"), "exit"])
monkeypatch.setattr("builtins.input", input_mock)
# Run dev mode
await run_dev_mode()
# Verify error handling
assert any("Type error: Invalid type" in msg for msg in prints)
assert any("Exiting developer mode" in msg for msg in prints)
@pytest.mark.asyncio
async def test_run_dev_mode_empty_input(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test empty input handling in run_dev_mode."""
monkeypatch.setenv("MCP_PROJECT_ROOT", "/tmp")
# Mock print function to capture output
prints: list[str] = []
def print_mock(x: object) -> None:
prints.append(str(x))
monkeypatch.setattr("builtins.print", print_mock)
# Mock input to return empty string then exit
input_mock = MagicMock(side_effect=["", "exit"])
monkeypatch.setattr("builtins.input", input_mock)
# Run dev mode
await run_dev_mode()
# Verify error handling
assert not any("Invalid command format" in msg for msg in prints)
assert any("Exiting developer mode" in msg for msg in prints)
@pytest.mark.asyncio
async def test_run_dev_mode_invalid_command(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test invalid command format handling in run_dev_mode."""
monkeypatch.setenv("MCP_PROJECT_ROOT", "/tmp")
# Mock print function to capture output
prints: list[str] = []
def print_mock(x: object) -> None:
prints.append(str(x))
monkeypatch.setattr("builtins.print", print_mock)
# Mock terminal to return None for parse_request
terminal = MockTerminal()
with patch(
"mcp_server_neurolorap.server.JsonRpcTerminal",
return_value=terminal,
):
# Mock input with invalid command then exit
input_mock = MagicMock(side_effect=["invalid command", "exit"])
monkeypatch.setattr("builtins.input", input_mock)
# Run dev mode
await run_dev_mode()
# Verify error handling
assert any("Invalid command format" in msg for msg in prints)
assert any("Exiting developer mode" in msg for msg in prints)
@pytest.mark.asyncio
async def test_run_dev_mode_unknown_command(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test unknown command handling in run_dev_mode."""
monkeypatch.setenv("MCP_PROJECT_ROOT", "/tmp")
# Mock JsonRpcTerminal class
with patch(
"mcp_server_neurolorap.server.JsonRpcTerminal"
) as mock_terminal_class:
terminal_instance = MockTerminal()
mock_terminal_class.return_value = terminal_instance
# Mock input to immediately exit
input_mock = MagicMock(side_effect=["unknown_command", "exit"])
monkeypatch.setattr("builtins.input", input_mock)
# Mock print function to capture output
prints: list[str] = []
def print_mock(x: object) -> None:
prints.append(str(x))
monkeypatch.setattr("builtins.print", print_mock)
# Run dev mode
await run_dev_mode()
# Verify error handling
error_msg = "Error: Method 'unknown_command' not found"
has_error = any(error_msg in msg for msg in prints)
has_exit = any("Exiting developer mode" in msg for msg in prints)
assert has_error, f"Expected '{error_msg}' in output"
assert has_exit, "Expected 'Exiting developer mode' message"
@pytest.mark.asyncio
async def test_run_dev_mode_keyboard_interrupt(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test keyboard interrupt handling in run_dev_mode."""
monkeypatch.setenv("MCP_PROJECT_ROOT", "/tmp")
# Mock input function to raise KeyboardInterrupt
input_mock = MagicMock(side_effect=KeyboardInterrupt)
monkeypatch.setattr("builtins.input", input_mock)
# Mock print function to capture output
prints: list[str] = []
def print_mock(x: object) -> None:
prints.append(str(x))
monkeypatch.setattr("builtins.print", print_mock)
# Run dev mode
await run_dev_mode()
# Verify error handling
assert any("Exiting developer mode" in msg for msg in prints)