MCP Server Neurolorap
by aindreyway
"""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!"