MCP Server Neurolorap

"""Unit tests for the JsonRpcTerminal class.""" from pathlib import Path from typing import Any, Dict from unittest.mock import MagicMock import pytest from mcp_server_neurolorap.terminal import JsonRpcTerminal # Type aliases for JSON-RPC structures JsonRpcRequest = Dict[str, Any] JsonRpcResponse = Dict[str, Any] JsonRpcError = Dict[str, Any] @pytest.fixture def terminal() -> JsonRpcTerminal: """Create a JsonRpcTerminal instance for testing.""" return JsonRpcTerminal() @pytest.fixture def terminal_with_root(project_root: Path) -> JsonRpcTerminal: """Create a JsonRpcTerminal instance with project root.""" return JsonRpcTerminal(project_root=str(project_root)) def test_init_basic(terminal: JsonRpcTerminal) -> None: """Test basic initialization of JsonRpcTerminal.""" assert terminal.project_root is None assert terminal.collector is not None assert set(terminal.commands.keys()) == { "help", "list_tools", "collect", "report", "exit", } def test_init_with_project_root( terminal_with_root: JsonRpcTerminal, project_root: Path ) -> None: """Test initialization with project root.""" # Check that project_root is set assert terminal_with_root.project_root is not None assert isinstance(terminal_with_root.project_root, Path) assert terminal_with_root.project_root.resolve() == project_root.resolve() # Check that collector is initialized with correct project_root assert terminal_with_root.collector is not None collector_root = terminal_with_root.collector.project_root assert isinstance(collector_root, Path) assert collector_root.resolve() == project_root.resolve() @pytest.mark.parametrize( "input_line,expected_method,expected_params", [ ("help", "help", []), ("collect src", "collect", ["src"]), ("collect src test-sub", "collect", ["src", "test-sub"]), ("list_tools", "list_tools", []), ("exit", "exit", []), ], ) def test_parse_request_valid( terminal: JsonRpcTerminal, input_line: str, expected_method: str, expected_params: list[str], ) -> None: """Test parsing valid requests.""" request = terminal.parse_request(input_line) assert request is not None assert request["jsonrpc"] == "2.0" assert request["method"] == expected_method assert request["params"] == expected_params assert isinstance(request["id"], int) @pytest.mark.parametrize( "input_line", [ "", # Empty line " ", # Whitespace only "\n", # Newline only None, # None value "invalid\x00command", # Invalid characters "command\nwith\nnewlines", # Multiple lines " spaces everywhere ", # Extra spaces ], ) def test_parse_request_invalid( terminal: JsonRpcTerminal, input_line: str ) -> None: """Test parsing invalid requests.""" assert terminal.parse_request(input_line) is None def test_format_response_success(terminal: JsonRpcTerminal) -> None: """Test formatting successful responses.""" result = "test result" response = terminal.format_response(result) assert response["jsonrpc"] == "2.0" assert response["result"] == result assert isinstance(response["id"], int) assert "error" not in response def test_format_response_error(terminal: JsonRpcTerminal) -> None: """Test formatting error responses.""" error: JsonRpcError = {"code": -32000, "message": "Test error"} response = terminal.format_response(None, error) assert response["jsonrpc"] == "2.0" assert response["error"] == error assert isinstance(response["id"], int) assert "result" not in response @pytest.mark.asyncio async def test_handle_command_unknown(terminal: JsonRpcTerminal) -> None: """Test handling unknown commands.""" request: JsonRpcRequest = {"jsonrpc": "2.0", "method": "unknown", "id": 1} response = await terminal.handle_command(request) assert "error" in response assert response["error"]["code"] == -32601 @pytest.mark.asyncio async def test_handle_command_error(terminal: JsonRpcTerminal) -> None: """Test handling command execution errors.""" # Mock collect command to raise an error terminal.commands["collect"] = MagicMock( side_effect=ValueError("Test error") ) request: JsonRpcRequest = {"jsonrpc": "2.0", "method": "collect", "id": 1} response = await terminal.handle_command(request) assert "error" in response assert "Test error" in response["error"]["message"] @pytest.mark.asyncio async def test_cmd_help(terminal: JsonRpcTerminal) -> None: """Test help command.""" result = await terminal.cmd_help([]) assert isinstance(result, str) assert "Available commands" in result assert "help" in result assert "list_tools" in result assert "collect" in result assert "exit" in result @pytest.mark.asyncio async def test_cmd_list_tools(terminal: JsonRpcTerminal) -> None: """Test list_tools command.""" result = await terminal.cmd_list_tools([]) assert isinstance(result, list) assert "code_collector" in result @pytest.mark.asyncio async def test_cmd_collect_no_params(terminal: JsonRpcTerminal) -> None: """Test collect command without parameters.""" with pytest.raises(ValueError, match="Path parameter required"): await terminal.cmd_collect([]) @pytest.mark.parametrize( "result_value,expected_type", [ ("string result", str), (123, int), ({"key": "value"}, dict), ([1, 2, 3], list), (None, type(None)), (True, bool), ], ) def test_format_response_different_types( terminal: JsonRpcTerminal, result_value: Any, expected_type: type ) -> None: """Test formatting responses with different result types.""" response = terminal.format_response(result_value) assert response["jsonrpc"] == "2.0" assert isinstance(response["result"], expected_type) assert response["result"] == result_value assert "error" not in response @pytest.mark.parametrize( "error_code,error_message", [ (-32700, "Parse error"), (-32600, "Invalid Request"), (-32601, "Method not found"), (-32602, "Invalid params"), (-32603, "Internal error"), (-32000, "Server error"), ], ) def test_format_response_different_errors( terminal: JsonRpcTerminal, error_code: int, error_message: str ) -> None: """Test formatting responses with different error types.""" error: JsonRpcError = {"code": error_code, "message": error_message} response = terminal.format_response(None, error) assert response["jsonrpc"] == "2.0" assert response["error"] == error assert "result" not in response @pytest.mark.asyncio async def test_handle_command_invalid_params( terminal: JsonRpcTerminal, ) -> None: """Test handling commands with invalid parameters.""" request: JsonRpcRequest = { "jsonrpc": "2.0", "method": "collect", "params": "invalid params type", # Should be a list "id": 1, } response = await terminal.handle_command(request) assert "error" in response assert response["error"]["code"] == -32602 @pytest.mark.asyncio async def test_cmd_collect_success( terminal_with_root: JsonRpcTerminal, project_root: Path ) -> None: """Test successful code collection.""" # Create test file test_file = project_root / "test.py" test_file.write_text("print('test')") try: result = await terminal_with_root.cmd_collect([str(test_file)]) assert isinstance(result, dict) assert "Code collection complete!" in result["result"] assert "Output file:" in result["result"] finally: # Cleanup test_file.unlink() @pytest.mark.parametrize( "path_input", [ "src/", # Directory path "./src", # Relative path "'quoted/path'", # Quoted path '"double/quoted/path"', # Double quoted path "path with spaces", # Path with spaces "multiple/path/segments", # Multiple segments ], ) @pytest.mark.asyncio async def test_cmd_collect_path_formats( terminal_with_root: JsonRpcTerminal, project_root: Path, path_input: str ) -> None: """Test code collection with different path formats.""" # Create test file test_dir = project_root / "test_dir" test_dir.mkdir(exist_ok=True) test_file = test_dir / "test.py" test_file.write_text("print('test')") try: # Replace path segments in input with actual test directory actual_path = str(test_dir) if path_input.startswith(("'", '"')): actual_path = f"{path_input[0]}{test_dir}{path_input[0]}" result = await terminal_with_root.cmd_collect([actual_path]) assert isinstance(result, dict) assert "Code collection complete!" in result["result"] finally: # Cleanup test_file.unlink() test_dir.rmdir() @pytest.mark.asyncio async def test_cmd_collect_with_subproject( terminal_with_root: JsonRpcTerminal, project_root: Path ) -> None: """Test code collection with subproject ID.""" # Create test file test_file = project_root / "test.py" test_file.write_text("print('test')") try: result = await terminal_with_root.cmd_collect( [str(test_file), "test-sub"] ) assert isinstance(result, dict) assert "Code collection complete!" in result["result"] assert "Subproject ID: test-sub" in result["result"] finally: # Cleanup test_file.unlink() @pytest.mark.asyncio async def test_cmd_collect_invalid_collector_creation( terminal: JsonRpcTerminal, ) -> None: """Test error handling when CodeCollector creation fails.""" # Create a terminal with an invalid project root invalid_terminal = JsonRpcTerminal(project_root="/nonexistent/path") with pytest.raises(ValueError): await invalid_terminal.cmd_collect(["some/path"]) @pytest.mark.asyncio async def test_cmd_collect_no_files( terminal_with_root: JsonRpcTerminal, ) -> None: """Test code collection with no files.""" with pytest.raises( ValueError, match="Failed to collect code or no files found" ): await terminal_with_root.cmd_collect(["nonexistent"]) @pytest.mark.asyncio async def test_cmd_exit(terminal: JsonRpcTerminal) -> None: """Test exit command.""" result = await terminal.cmd_exit([]) assert result == "Goodbye!" @pytest.mark.asyncio async def test_command_execution_flow(terminal: JsonRpcTerminal) -> None: """Test complete command execution flow.""" # Test help command request = terminal.parse_request("help") assert request is not None response = await terminal.handle_command(request) assert "result" in response assert isinstance(response["result"], str) assert "Available commands" in response["result"] # Test list_tools command request = terminal.parse_request("list_tools") assert request is not None response = await terminal.handle_command(request) assert "result" in response assert isinstance(response["result"], list) assert "code_collector" in response["result"] # Test exit command request = terminal.parse_request("exit") assert request is not None response = await terminal.handle_command(request) assert "result" in response assert response["result"] == "Goodbye!"