MCP Server Neurolorap

"""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)