"""Tests for MCP resource handlers.""" import pytest import json from unittest.mock import Mock, AsyncMock, patch from mcp_forge.server.resources import ResourceHandler @pytest.fixture def mock_client_manager(): """Mock MCP client manager.""" manager = Mock() manager.list_all_tools = AsyncMock(return_value=["read_file", "write_file", "calculate"]) return manager @pytest.fixture def mock_session_manager(): """Mock session manager.""" manager = Mock() # Mock session state mock_state = Mock() mock_state.session_id = "test-session" mock_state.documented_variables = {"x": "Test variable", "result": "Calculation result"} mock_state.note = "Test session state" mock_state.all_variables = ["x", "y", "result", "np", "pd"] mock_state.introspection = { "x": {"type": "int", "size": 28}, "result": {"type": "float", "size": 24} } mock_state.to_dict = Mock(return_value={ "session_id": "test-session", "documented_variables": {"x": "Test variable", "result": "Calculation result"}, "note": "Test session state", "all_variables": ["x", "y", "result", "np", "pd"], "introspection": { "x": {"type": "int", "size": 28}, "result": {"type": "float", "size": 24} }, "last_updated": "2026-02-06T12:00:00Z" }) manager.get_session_state = Mock(return_value=mock_state) manager.list_sessions = Mock(return_value=[ {"session_id": "test-session", "created_at": "2026-02-06T12:00:00Z"} ]) return manager @pytest.fixture def mock_environment_builder(): """Mock environment builder.""" builder = Mock() builder.list_templates = Mock(return_value={ "datascience": { "description": "Data science environment with numpy, pandas, matplotlib", "packages": ["numpy>=1.24", "pandas>=2.0", "matplotlib>=3.7"] }, "ml": { "description": "Machine learning environment", "packages": ["scikit-learn>=1.3", "tensorflow>=2.13"] } }) return builder @pytest.fixture def mock_config(): """Mock forge configuration.""" config = Mock() config.execution = Mock() config.execution.default_backend = "simple" config.execution.default_timeout = 300 config.sessions = Mock() config.sessions.max_concurrent = 10 # Add environment-related config for environment/info resource config.max_packages = 50 config.max_build_time = 300 config.base_images = {"python:3.11": {}, "python:3.12": {}} return config @pytest.fixture def resource_handler(mock_client_manager, mock_session_manager, mock_environment_builder, mock_config): """Create ResourceHandler instance with mocked dependencies.""" return ResourceHandler( client_manager=mock_client_manager, session_manager=mock_session_manager, environment_builder=mock_environment_builder, config=mock_config ) @pytest.mark.asyncio async def test_handle_tools_available(resource_handler, mock_client_manager): """Test handling tools/available resource returns list of available tools.""" result = await resource_handler.handle_resource("mcp://forge/tools/available") assert str(result.uri) == "mcp://forge/tools/available" assert result.mimeType == "application/json" # Parse JSON content tools = json.loads(result.text) assert tools == {"tools": ["read_file", "write_file", "calculate"]} # Verify client manager was called mock_client_manager.list_all_tools.assert_awaited_once() @pytest.mark.asyncio async def test_handle_session_state(resource_handler, mock_session_manager): """Test handling session state resource returns documented state.""" session_id = "test-session" result = await resource_handler.handle_resource(f"mcp://forge/sessions/{session_id}/state") assert str(result.uri) == f"mcp://forge/sessions/{session_id}/state" assert result.mimeType == "application/json" # Parse JSON content state = json.loads(result.text) assert state["session_id"] == session_id assert state["documented_variables"] == {"x": "Test variable", "result": "Calculation result"} assert state["note"] == "Test session state" assert "last_updated" in state # Verify session manager was called mock_session_manager.get_session_state.assert_called_once_with(session_id) @pytest.mark.asyncio async def test_handle_session_variables(resource_handler, mock_session_manager): """Test handling session variables resource returns list of variables.""" session_id = "test-session" result = await resource_handler.handle_resource(f"mcp://forge/sessions/{session_id}/variables") assert str(result.uri) == f"mcp://forge/sessions/{session_id}/variables" assert result.mimeType == "application/json" # Parse JSON content data = json.loads(result.text) assert data["variables"] == ["x", "y", "result", "np", "pd"] # Verify session manager was called mock_session_manager.get_session_state.assert_called_once_with(session_id) @pytest.mark.asyncio async def test_handle_environments_list(resource_handler, mock_environment_builder): """Test handling environments/list resource returns templates and built environments.""" result = await resource_handler.handle_resource("mcp://forge/environments/list") assert str(result.uri) == "mcp://forge/environments/list" assert result.mimeType == "application/json" # Parse JSON content environments = json.loads(result.text) assert "templates" in environments assert "datascience" in environments["templates"] assert "ml" in environments["templates"] assert environments["templates"]["datascience"]["description"] == "Data science environment with numpy, pandas, matplotlib" # Verify environment builder was called mock_environment_builder.list_templates.assert_called_once() @pytest.mark.asyncio async def test_handle_environment_info(resource_handler, mock_config): """Test handling environment info resource returns configuration info.""" result = await resource_handler.handle_resource("mcp://forge/environment/info") assert str(result.uri) == "mcp://forge/environment/info" assert result.mimeType == "application/json" # Parse JSON content info = json.loads(result.text) assert "base_images" in info assert "python:3.11" in info["base_images"] assert "python:3.12" in info["base_images"] assert info["max_packages"] == 50 assert info["max_build_time"] == 300 @pytest.mark.asyncio async def test_handle_sessions_list(resource_handler, mock_session_manager): """Test handling sessions/list resource returns list of active sessions.""" result = await resource_handler.handle_resource("mcp://forge/sessions/list") assert str(result.uri) == "mcp://forge/sessions/list" assert result.mimeType == "application/json" # Parse JSON content data = json.loads(result.text) assert len(data["sessions"]) == 1 assert data["sessions"][0]["session_id"] == "test-session" # Verify session manager was called mock_session_manager.list_sessions.assert_called_once() @pytest.mark.asyncio async def test_handle_unknown_resource(resource_handler): """Test handling unknown resource raises ValueError.""" with pytest.raises(ValueError, match="Unknown resource URI"): await resource_handler.handle_resource("mcp://forge/unknown/resource") @pytest.mark.asyncio async def test_handle_session_not_found(resource_handler, mock_session_manager): """Test handling session resource when session doesn't exist raises KeyError.""" mock_session_manager.get_session_state.side_effect = KeyError("Session not found") with pytest.raises(KeyError, match="Session not found"): await resource_handler.handle_resource("mcp://forge/sessions/nonexistent/state") @pytest.mark.asyncio async def test_parse_uri_tools_available(resource_handler): """Test URI parsing for tools/available resource.""" resource_type, params = resource_handler._parse_uri("mcp://forge/tools/available") assert resource_type == "tools_available" assert params == {} @pytest.mark.asyncio async def test_parse_uri_session_state(resource_handler): """Test URI parsing for session state resource.""" resource_type, params = resource_handler._parse_uri("mcp://forge/sessions/test-123/state") assert resource_type == "session_state" assert params == {"session_id": "test-123"} @pytest.mark.asyncio async def test_parse_uri_session_variables(resource_handler): """Test URI parsing for session variables resource.""" resource_type, params = resource_handler._parse_uri("mcp://forge/sessions/test-123/variables") assert resource_type == "session_variables" assert params == {"session_id": "test-123"} @pytest.mark.asyncio async def test_parse_uri_invalid_format(resource_handler): """Test URI parsing with invalid format raises ValueError.""" with pytest.raises(ValueError, match="Invalid URI format"): resource_handler._parse_uri("invalid://uri") @pytest.mark.asyncio async def test_json_serialization_valid(resource_handler): """Test that all returned JSON content is valid and can be parsed.""" # Test all resource types return valid JSON resources = [ "mcp://forge/tools/available", "mcp://forge/sessions/test-session/state", "mcp://forge/sessions/test-session/variables", "mcp://forge/environments/list", "mcp://forge/environment/info", "mcp://forge/sessions/list" ] for uri in resources: result = await resource_handler.handle_resource(uri) # Should not raise exception parsed = json.loads(result.text) assert parsed is not None