mirror of
https://github.com/coleam00/Archon
synced 2026-04-29 17:38:11 +00:00
Comprehensive update to MCP server error handling: Error Handling Improvements: - Applied MCPErrorFormatter to all remaining MCP tool files - Replaced all hardcoded timeout values with configurable timeout system - Converted all simple string errors to structured error format - Added proper httpx exception handling with detailed context Tools Updated: - document_tools.py: All 5 document management tools - version_tools.py: All 4 version management tools - feature_tools.py: Project features tool - project_tools.py: Remaining 3 project tools (get, list, delete) - task_tools.py: Remaining 4 task tools (get, list, update, delete) Test Improvements: - Removed backward compatibility checks from all tests - Tests now enforce structured error format (dict not string) - Any string error response is now considered a bug - All 20 tests passing with new strict validation This completes the error handling refactor for all MCP tools, ensuring consistent client experience and better debugging.
127 lines
No EOL
4.6 KiB
Python
127 lines
No EOL
4.6 KiB
Python
"""Unit tests for feature management tools."""
|
|
|
|
import json
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from mcp.server.fastmcp import Context
|
|
|
|
from src.mcp_server.features.feature_tools import register_feature_tools
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_mcp():
|
|
"""Create a mock MCP server for testing."""
|
|
mock = MagicMock()
|
|
# Store registered tools
|
|
mock._tools = {}
|
|
|
|
def tool_decorator():
|
|
def decorator(func):
|
|
mock._tools[func.__name__] = func
|
|
return func
|
|
return decorator
|
|
|
|
mock.tool = tool_decorator
|
|
return mock
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_context():
|
|
"""Create a mock context for testing."""
|
|
return MagicMock(spec=Context)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_project_features_success(mock_mcp, mock_context):
|
|
"""Test successful retrieval of project features."""
|
|
register_feature_tools(mock_mcp)
|
|
|
|
# Get the get_project_features function
|
|
get_project_features = mock_mcp._tools.get('get_project_features')
|
|
|
|
assert get_project_features is not None, "get_project_features tool not registered"
|
|
|
|
# Mock HTTP response with various feature structures
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"features": [
|
|
{"name": "authentication", "status": "completed", "components": ["oauth", "jwt"]},
|
|
{"name": "api", "status": "in_progress", "endpoints_done": 12, "endpoints_total": 20},
|
|
{"name": "database", "status": "planned"},
|
|
{"name": "payments", "provider": "stripe", "version": "2.0", "enabled": True}
|
|
]
|
|
}
|
|
|
|
with patch('src.mcp_server.features.feature_tools.httpx.AsyncClient') as mock_client:
|
|
mock_async_client = AsyncMock()
|
|
mock_async_client.get.return_value = mock_response
|
|
mock_client.return_value.__aenter__.return_value = mock_async_client
|
|
|
|
result = await get_project_features(mock_context, project_id="project-123")
|
|
|
|
result_data = json.loads(result)
|
|
assert result_data["success"] is True
|
|
assert result_data["count"] == 4
|
|
assert len(result_data["features"]) == 4
|
|
|
|
# Verify different feature structures are preserved
|
|
features = result_data["features"]
|
|
assert features[0]["components"] == ["oauth", "jwt"]
|
|
assert features[1]["endpoints_done"] == 12
|
|
assert features[2]["status"] == "planned"
|
|
assert features[3]["provider"] == "stripe"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_project_features_empty(mock_mcp, mock_context):
|
|
"""Test getting features for a project with no features defined."""
|
|
register_feature_tools(mock_mcp)
|
|
|
|
get_project_features = mock_mcp._tools.get('get_project_features')
|
|
|
|
# Mock response with empty features
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"features": []}
|
|
|
|
with patch('src.mcp_server.features.feature_tools.httpx.AsyncClient') as mock_client:
|
|
mock_async_client = AsyncMock()
|
|
mock_async_client.get.return_value = mock_response
|
|
mock_client.return_value.__aenter__.return_value = mock_async_client
|
|
|
|
result = await get_project_features(mock_context, project_id="project-123")
|
|
|
|
result_data = json.loads(result)
|
|
assert result_data["success"] is True
|
|
assert result_data["count"] == 0
|
|
assert result_data["features"] == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_project_features_not_found(mock_mcp, mock_context):
|
|
"""Test getting features for a non-existent project."""
|
|
register_feature_tools(mock_mcp)
|
|
|
|
get_project_features = mock_mcp._tools.get('get_project_features')
|
|
|
|
# Mock 404 response
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 404
|
|
mock_response.text = "Project not found"
|
|
|
|
with patch('src.mcp_server.features.feature_tools.httpx.AsyncClient') as mock_client:
|
|
mock_async_client = AsyncMock()
|
|
mock_async_client.get.return_value = mock_response
|
|
mock_client.return_value.__aenter__.return_value = mock_async_client
|
|
|
|
result = await get_project_features(mock_context, project_id="non-existent")
|
|
|
|
result_data = json.loads(result)
|
|
assert result_data["success"] is False
|
|
# Error must be structured format (dict), not string
|
|
assert "error" in result_data
|
|
assert isinstance(result_data["error"], dict), "Error should be structured format, not string"
|
|
assert result_data["error"]["type"] == "not_found"
|
|
assert "not found" in result_data["error"]["message"].lower() |