initial commit after one day coding agent session
This commit is contained in:
commit
372af75b90
88 changed files with 22694 additions and 0 deletions
266
tests/server/test_resources.py
Normal file
266
tests/server/test_resources.py
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
"""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
|
||||
203
tests/server/test_server.py
Normal file
203
tests/server/test_server.py
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
"""Tests for MCP Forge Server."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock, patch
|
||||
from pathlib import Path
|
||||
|
||||
from mcp_forge.server.server import ForgeServer
|
||||
from mcp_forge.config.schema import ForgeConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(tmp_path):
|
||||
"""Mock forge configuration."""
|
||||
config = Mock(spec=ForgeConfig)
|
||||
|
||||
# Server config
|
||||
config.server = Mock()
|
||||
config.server.host = "localhost"
|
||||
config.server.port = 3000
|
||||
config.server.podman_socket = Path("/run/user/1000/podman/podman.sock")
|
||||
|
||||
# Security config
|
||||
config.security = Mock()
|
||||
config.security.audit_log = tmp_path / "audit.log"
|
||||
config.security.max_memory = "2g"
|
||||
config.security.max_timeout = 1800
|
||||
|
||||
# Execution config
|
||||
config.execution = Mock()
|
||||
config.execution.default_backend = "simple"
|
||||
config.execution.default_timeout = 300
|
||||
config.execution.max_timeout = 1800
|
||||
config.execution.default_memory = "512m"
|
||||
config.execution.max_memory = "2g"
|
||||
|
||||
# Sessions config
|
||||
config.sessions = Mock()
|
||||
config.sessions.max_concurrent = 10
|
||||
config.sessions.idle_timeout = 3600
|
||||
|
||||
# Environment builder config
|
||||
config.environment_builder = Mock()
|
||||
config.environment_builder.uv_cache_path = tmp_path / "cache"
|
||||
config.environment_builder.max_build_time = 600
|
||||
config.environment_builder.package_validation = Mock()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_server_initialization(mock_config):
|
||||
"""Test that server initializes all components."""
|
||||
with patch('mcp_forge.server.server.AuditLogger'), \
|
||||
patch('mcp_forge.server.server.OperationValidator'), \
|
||||
patch('mcp_forge.server.server.PodmanClient'), \
|
||||
patch('mcp_forge.server.server.SecureContainerManager'), \
|
||||
patch('mcp_forge.server.server.MCPClientManager'), \
|
||||
patch('mcp_forge.server.server.ToolBridgeServer'), \
|
||||
patch('mcp_forge.server.server.ToolInjectionGenerator'), \
|
||||
patch('mcp_forge.server.server.SimpleBackend'), \
|
||||
patch('mcp_forge.server.server.JupyterKernelManager'), \
|
||||
patch('mcp_forge.server.server.JupyterBackend'), \
|
||||
patch('mcp_forge.server.server.SessionManager'), \
|
||||
patch('mcp_forge.server.server.EnvironmentBuilder'):
|
||||
|
||||
server = ForgeServer(config=mock_config)
|
||||
|
||||
# Verify server was created
|
||||
assert server is not None
|
||||
assert server.config == mock_config
|
||||
|
||||
# Verify components were initialized
|
||||
assert hasattr(server, 'audit_logger')
|
||||
assert hasattr(server, 'operation_validator')
|
||||
assert hasattr(server, 'podman_client')
|
||||
assert hasattr(server, 'container_manager')
|
||||
assert hasattr(server, 'client_manager')
|
||||
assert hasattr(server, 'bridge_server')
|
||||
assert hasattr(server, 'simple_backend')
|
||||
assert hasattr(server, 'jupyter_backend')
|
||||
assert hasattr(server, 'environment_builder')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tools_registration(mock_config):
|
||||
"""Test that tools are registered with the server."""
|
||||
with patch('mcp_forge.server.server.AuditLogger'), \
|
||||
patch('mcp_forge.server.server.OperationValidator'), \
|
||||
patch('mcp_forge.server.server.PodmanClient'), \
|
||||
patch('mcp_forge.server.server.SecureContainerManager'), \
|
||||
patch('mcp_forge.server.server.MCPClientManager'), \
|
||||
patch('mcp_forge.server.server.ToolBridgeServer'), \
|
||||
patch('mcp_forge.server.server.ToolInjectionGenerator'), \
|
||||
patch('mcp_forge.server.server.SimpleBackend'), \
|
||||
patch('mcp_forge.server.server.JupyterKernelManager'), \
|
||||
patch('mcp_forge.server.server.JupyterBackend'), \
|
||||
patch('mcp_forge.server.server.SessionManager'), \
|
||||
patch('mcp_forge.server.server.EnvironmentBuilder'):
|
||||
|
||||
server = ForgeServer(config=mock_config)
|
||||
|
||||
# Verify tool instances were created
|
||||
assert hasattr(server, 'execute_python_tool')
|
||||
assert hasattr(server, 'document_state_tool')
|
||||
assert hasattr(server, 'build_environment_tool')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resources_registration(mock_config):
|
||||
"""Test that resources are registered with the server."""
|
||||
with patch('mcp_forge.server.server.AuditLogger'), \
|
||||
patch('mcp_forge.server.server.OperationValidator'), \
|
||||
patch('mcp_forge.server.server.PodmanClient'), \
|
||||
patch('mcp_forge.server.server.SecureContainerManager'), \
|
||||
patch('mcp_forge.server.server.MCPClientManager'), \
|
||||
patch('mcp_forge.server.server.ToolBridgeServer'), \
|
||||
patch('mcp_forge.server.server.ToolInjectionGenerator'), \
|
||||
patch('mcp_forge.server.server.SimpleBackend'), \
|
||||
patch('mcp_forge.server.server.JupyterKernelManager'), \
|
||||
patch('mcp_forge.server.server.JupyterBackend'), \
|
||||
patch('mcp_forge.server.server.SessionManager'), \
|
||||
patch('mcp_forge.server.server.EnvironmentBuilder'):
|
||||
|
||||
server = ForgeServer(config=mock_config)
|
||||
|
||||
# Verify resource handler was created
|
||||
assert hasattr(server, 'resource_handler')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_server_shutdown(mock_config):
|
||||
"""Test that server shuts down gracefully."""
|
||||
with patch('mcp_forge.server.server.AuditLogger') as mock_audit, \
|
||||
patch('mcp_forge.server.server.OperationValidator'), \
|
||||
patch('mcp_forge.server.server.PodmanClient'), \
|
||||
patch('mcp_forge.server.server.SecureContainerManager'), \
|
||||
patch('mcp_forge.server.server.MCPClientManager') as mock_client_mgr, \
|
||||
patch('mcp_forge.server.server.ToolBridgeServer') as mock_bridge, \
|
||||
patch('mcp_forge.server.server.ToolInjectionGenerator'), \
|
||||
patch('mcp_forge.server.server.SimpleBackend'), \
|
||||
patch('mcp_forge.server.server.JupyterKernelManager'), \
|
||||
patch('mcp_forge.server.server.JupyterBackend'), \
|
||||
patch('mcp_forge.server.server.SessionManager'), \
|
||||
patch('mcp_forge.server.server.EnvironmentBuilder'):
|
||||
|
||||
# Setup mocks
|
||||
mock_client_mgr.return_value.shutdown = AsyncMock()
|
||||
mock_bridge_instance = Mock()
|
||||
mock_bridge_instance.stop = Mock() # Not async
|
||||
mock_bridge.return_value = mock_bridge_instance
|
||||
mock_audit_instance = Mock()
|
||||
mock_audit.return_value = mock_audit_instance
|
||||
|
||||
server = ForgeServer(config=mock_config)
|
||||
|
||||
# Shutdown server
|
||||
await server.shutdown()
|
||||
|
||||
# Verify cleanup was called
|
||||
server.client_manager.shutdown.assert_called_once()
|
||||
server.bridge_server.stop.assert_called_once()
|
||||
server.audit_logger.log.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_server_component_initialization_order(mock_config):
|
||||
"""Test that components are initialized in correct order."""
|
||||
init_order = []
|
||||
|
||||
def track_init(name):
|
||||
def decorator(cls):
|
||||
original_init = cls.__init__
|
||||
def new_init(self, *args, **kwargs):
|
||||
init_order.append(name)
|
||||
return original_init(self, *args, **kwargs)
|
||||
cls.__init__ = new_init
|
||||
return cls
|
||||
return decorator
|
||||
|
||||
with patch('mcp_forge.server.server.AuditLogger') as mock_audit, \
|
||||
patch('mcp_forge.server.server.OperationValidator') as mock_validator, \
|
||||
patch('mcp_forge.server.server.PodmanClient') as mock_podman, \
|
||||
patch('mcp_forge.server.server.SecureContainerManager') as mock_container, \
|
||||
patch('mcp_forge.server.server.MCPClientManager'), \
|
||||
patch('mcp_forge.server.server.ToolBridgeServer'), \
|
||||
patch('mcp_forge.server.server.ToolInjectionGenerator'), \
|
||||
patch('mcp_forge.server.server.SimpleBackend'), \
|
||||
patch('mcp_forge.server.server.JupyterKernelManager'), \
|
||||
patch('mcp_forge.server.server.JupyterBackend'), \
|
||||
patch('mcp_forge.server.server.SessionManager'), \
|
||||
patch('mcp_forge.server.server.EnvironmentBuilder'):
|
||||
|
||||
mock_audit.side_effect = lambda *args, **kwargs: init_order.append('audit_logger') or Mock()
|
||||
mock_validator.side_effect = lambda *args, **kwargs: init_order.append('operation_validator') or Mock()
|
||||
mock_podman.side_effect = lambda *args, **kwargs: init_order.append('podman_client') or Mock()
|
||||
mock_container.side_effect = lambda *args, **kwargs: init_order.append('container_manager') or Mock()
|
||||
|
||||
server = ForgeServer(config=mock_config)
|
||||
|
||||
# Verify security components are initialized first
|
||||
assert init_order.index('audit_logger') < init_order.index('podman_client')
|
||||
assert init_order.index('operation_validator') < init_order.index('podman_client')
|
||||
assert init_order.index('podman_client') < init_order.index('container_manager')
|
||||
86
tests/server/test_server_integration.py
Normal file
86
tests/server/test_server_integration.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
"""Integration tests for MCP Forge Server - tests real component initialization."""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from mcp_forge.server.server import ForgeServer
|
||||
from mcp_forge.config.schema import (
|
||||
ForgeConfig, ServerConfig, SecurityConfig, ExecutionConfig, SessionConfig,
|
||||
ImageConfig, VolumeConfig, EnvironmentBuilderConfig, PackageValidationConfig
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def real_config(tmp_path):
|
||||
"""Real configuration with all required fields."""
|
||||
# Create required files
|
||||
(tmp_path / "allowlist.txt").write_text("requests\npandas\nnumpy\n")
|
||||
(tmp_path / "blocklist.txt").write_text("")
|
||||
|
||||
config = ForgeConfig(
|
||||
server=ServerConfig(
|
||||
host="localhost",
|
||||
port=3000,
|
||||
podman_socket=Path("/run/user/1000/podman/podman.sock")
|
||||
),
|
||||
security=SecurityConfig(
|
||||
audit_log=tmp_path / "audit.log",
|
||||
enforce_resource_limits=True,
|
||||
allow_network=False
|
||||
),
|
||||
execution=ExecutionConfig(
|
||||
default_backend="simple",
|
||||
default_timeout=300,
|
||||
max_timeout=1800,
|
||||
default_memory="512m",
|
||||
max_memory="2g"
|
||||
),
|
||||
images=ImageConfig(
|
||||
allowed_python_versions=["3.11", "3.12"],
|
||||
default_base_image="python:3.11"
|
||||
),
|
||||
sessions=SessionConfig(
|
||||
max_concurrent=10,
|
||||
idle_timeout=3600
|
||||
),
|
||||
volumes=VolumeConfig(
|
||||
base_path=tmp_path / "volumes"
|
||||
),
|
||||
environment_builder=EnvironmentBuilderConfig(
|
||||
uv_cache_path=tmp_path / "cache",
|
||||
build_rate_limit={"requests": 5, "period": 60},
|
||||
package_validation=PackageValidationConfig(
|
||||
allowlist_path=tmp_path / "allowlist.txt",
|
||||
blocklist_path=tmp_path / "blocklist.txt"
|
||||
)
|
||||
),
|
||||
mcp_tools={}
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_server_can_initialize_with_minimal_mocking(real_config):
|
||||
"""Test that ForgeServer can initialize with real component instances."""
|
||||
# Only mock the podman library since we don't have a real Podman socket
|
||||
with patch('mcp_forge.podman.client.BasePodmanClient'):
|
||||
# This should succeed if all parameter mismatches are fixed
|
||||
server = ForgeServer(config=real_config)
|
||||
|
||||
# Verify all components were created
|
||||
assert server.audit_logger is not None
|
||||
assert server.operation_validator is not None
|
||||
assert server.podman_client is not None
|
||||
assert server.container_manager is not None
|
||||
assert server.client_manager is not None
|
||||
assert server.bridge_server is not None
|
||||
assert server.simple_backend is not None
|
||||
assert server.kernel_manager is not None
|
||||
assert server.session_manager is not None
|
||||
assert server.jupyter_backend is not None
|
||||
assert server.environment_builder is not None
|
||||
# Sub-components created by EnvironmentBuilder
|
||||
assert server.package_validator is not None
|
||||
assert server.uv_installer is not None
|
||||
assert server.image_builder is not None
|
||||
250
tests/server/tools/test_build_environment.py
Normal file
250
tests/server/tools/test_build_environment.py
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
"""Tests for Build Custom Environment Tool."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock
|
||||
from mcp.types import Tool, TextContent
|
||||
import json
|
||||
|
||||
from mcp_forge.server.tools.build_environment import BuildEnvironmentTool
|
||||
from mcp_forge.builder.environment_builder import BuildResult
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_environment_builder():
|
||||
"""Mock environment builder."""
|
||||
builder = Mock()
|
||||
builder.build_environment = AsyncMock(return_value=BuildResult(
|
||||
success=True,
|
||||
image_name="mcp-forge/custom:test-env",
|
||||
image_id="sha256:abc123",
|
||||
build_time=45.2,
|
||||
size_bytes=524288000, # 500MB
|
||||
installed_packages=["numpy==1.24.0", "pandas==2.0.0"],
|
||||
cache_hit=False
|
||||
))
|
||||
return builder
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_audit_logger():
|
||||
"""Mock audit logger."""
|
||||
logger = Mock()
|
||||
logger.log_environment_build = Mock()
|
||||
return logger
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def build_environment_tool(mock_environment_builder, mock_audit_logger):
|
||||
"""Create BuildEnvironmentTool instance with mocked dependencies."""
|
||||
return BuildEnvironmentTool(
|
||||
environment_builder=mock_environment_builder,
|
||||
audit_logger=mock_audit_logger
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_tool_definition(build_environment_tool):
|
||||
"""Test that tool definition matches MCP spec."""
|
||||
definition = build_environment_tool.get_tool_definition()
|
||||
|
||||
assert isinstance(definition, Tool)
|
||||
assert definition.name == "build_custom_environment"
|
||||
assert definition.description is not None
|
||||
assert "build" in definition.description.lower()
|
||||
|
||||
# Verify required schema properties
|
||||
schema = definition.inputSchema
|
||||
assert schema["type"] == "object"
|
||||
assert "name" in schema["properties"]
|
||||
assert "packages" in schema["properties"]
|
||||
assert "base_image" in schema["properties"]
|
||||
assert "python_version" in schema["properties"]
|
||||
assert "description" in schema["properties"]
|
||||
assert "name" in schema["required"]
|
||||
assert "packages" in schema["required"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_with_valid_packages(
|
||||
build_environment_tool,
|
||||
mock_environment_builder,
|
||||
mock_audit_logger
|
||||
):
|
||||
"""Test building environment with valid packages."""
|
||||
arguments = {
|
||||
"name": "test-ml-env",
|
||||
"packages": ["numpy>=1.24.0", "pandas>=2.0.0"]
|
||||
}
|
||||
|
||||
result = await build_environment_tool.execute(arguments)
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TextContent)
|
||||
|
||||
# Parse JSON response
|
||||
response = json.loads(result[0].text)
|
||||
assert response["success"] is True
|
||||
assert response["image_name"] == "mcp-forge/custom:test-env"
|
||||
assert response["build_time"] == 45.2
|
||||
assert len(response["installed_packages"]) == 2
|
||||
|
||||
# Verify builder was called
|
||||
mock_environment_builder.build_environment.assert_called_once()
|
||||
|
||||
# Verify audit log was called
|
||||
mock_audit_logger.log_environment_build.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_with_base_image(
|
||||
build_environment_tool,
|
||||
mock_environment_builder
|
||||
):
|
||||
"""Test building with custom base image."""
|
||||
arguments = {
|
||||
"name": "test-env",
|
||||
"packages": ["requests"],
|
||||
"base_image": "python:3.12"
|
||||
}
|
||||
|
||||
result = await build_environment_tool.execute(arguments)
|
||||
|
||||
# Parse JSON response
|
||||
response = json.loads(result[0].text)
|
||||
assert response["success"] is True
|
||||
|
||||
# Verify base_image was passed to builder
|
||||
call_args = mock_environment_builder.build_environment.call_args
|
||||
assert call_args.kwargs["base_image"] == "python:3.12"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_with_python_version(
|
||||
build_environment_tool,
|
||||
mock_environment_builder
|
||||
):
|
||||
"""Test building with specific Python version."""
|
||||
arguments = {
|
||||
"name": "test-env",
|
||||
"packages": ["numpy"],
|
||||
"python_version": "3.12"
|
||||
}
|
||||
|
||||
result = await build_environment_tool.execute(arguments)
|
||||
|
||||
# Parse JSON response
|
||||
response = json.loads(result[0].text)
|
||||
assert response["success"] is True
|
||||
|
||||
# Verify python_version was passed to builder
|
||||
call_args = mock_environment_builder.build_environment.call_args
|
||||
assert call_args.kwargs["python_version"] == "3.12"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_with_description(
|
||||
build_environment_tool,
|
||||
mock_environment_builder
|
||||
):
|
||||
"""Test building with description."""
|
||||
arguments = {
|
||||
"name": "test-env",
|
||||
"packages": ["pandas"],
|
||||
"description": "Environment for data analysis"
|
||||
}
|
||||
|
||||
result = await build_environment_tool.execute(arguments)
|
||||
|
||||
# Parse JSON response
|
||||
response = json.loads(result[0].text)
|
||||
assert response["success"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_arguments_missing_name(build_environment_tool):
|
||||
"""Test that validation fails when name is missing."""
|
||||
arguments = {
|
||||
"packages": ["numpy"]
|
||||
}
|
||||
|
||||
with pytest.raises((ValueError, KeyError)):
|
||||
await build_environment_tool.execute(arguments)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_arguments_missing_packages(build_environment_tool):
|
||||
"""Test that validation fails when packages is missing."""
|
||||
arguments = {
|
||||
"name": "test-env"
|
||||
}
|
||||
|
||||
with pytest.raises((ValueError, KeyError)):
|
||||
await build_environment_tool.execute(arguments)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_arguments_invalid_name(build_environment_tool):
|
||||
"""Test that validation fails for invalid environment name."""
|
||||
arguments = {
|
||||
"name": "invalid name with spaces",
|
||||
"packages": ["numpy"]
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="name"):
|
||||
await build_environment_tool.execute(arguments)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_arguments_invalid_packages_type(build_environment_tool):
|
||||
"""Test that validation fails when packages is not a list."""
|
||||
arguments = {
|
||||
"name": "test-env",
|
||||
"packages": "not-a-list"
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="packages"):
|
||||
await build_environment_tool.execute(arguments)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_arguments_empty_packages(build_environment_tool):
|
||||
"""Test that validation fails when packages list is empty."""
|
||||
arguments = {
|
||||
"name": "test-env",
|
||||
"packages": []
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="packages"):
|
||||
await build_environment_tool.execute(arguments)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_error_handling(
|
||||
build_environment_tool,
|
||||
mock_environment_builder
|
||||
):
|
||||
"""Test that build errors are handled gracefully."""
|
||||
mock_environment_builder.build_environment = AsyncMock(return_value=BuildResult(
|
||||
success=False,
|
||||
image_name="",
|
||||
image_id="",
|
||||
build_time=5.0,
|
||||
size_bytes=0,
|
||||
installed_packages=[],
|
||||
cache_hit=False,
|
||||
error="Package 'invalid-pkg' not found"
|
||||
))
|
||||
|
||||
arguments = {
|
||||
"name": "test-env",
|
||||
"packages": ["invalid-pkg"]
|
||||
}
|
||||
|
||||
result = await build_environment_tool.execute(arguments)
|
||||
|
||||
# Parse JSON response
|
||||
response = json.loads(result[0].text)
|
||||
assert response["success"] is False
|
||||
assert "error" in response
|
||||
assert "invalid-pkg" in response["error"]
|
||||
188
tests/server/tools/test_document_state.py
Normal file
188
tests/server/tools/test_document_state.py
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
"""Tests for Document State Tool."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock
|
||||
from mcp.types import Tool, TextContent
|
||||
import json
|
||||
|
||||
from mcp_forge.server.tools.document_state import DocumentStateTool
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session_manager():
|
||||
"""Mock session manager."""
|
||||
manager = Mock()
|
||||
# Mock get_session_state to return a session with state
|
||||
mock_session = Mock()
|
||||
mock_session.state = Mock()
|
||||
mock_session.state.to_dict = Mock(return_value={
|
||||
"variables": {"x": 1, "y": 2},
|
||||
"documented_variables": {},
|
||||
"note": None
|
||||
})
|
||||
manager.get_session_state = Mock(return_value=mock_session)
|
||||
manager.document_variables = AsyncMock(return_value={"success": True, "documented_count": 2})
|
||||
manager.session_exists = Mock(return_value=True)
|
||||
return manager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def document_state_tool(mock_session_manager):
|
||||
"""Create DocumentStateTool instance with mocked dependencies."""
|
||||
return DocumentStateTool(session_manager=mock_session_manager)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_tool_definition(document_state_tool):
|
||||
"""Test that tool definition matches MCP spec."""
|
||||
definition = document_state_tool.get_tool_definition()
|
||||
|
||||
assert isinstance(definition, Tool)
|
||||
assert definition.name == "document_state"
|
||||
assert definition.description is not None
|
||||
assert "document" in definition.description.lower()
|
||||
|
||||
# Verify required schema properties
|
||||
schema = definition.inputSchema
|
||||
assert schema["type"] == "object"
|
||||
assert "session_id" in schema["properties"]
|
||||
assert "variables" in schema["properties"]
|
||||
assert "note" in schema["properties"]
|
||||
assert "clear" in schema["properties"]
|
||||
assert "session_id" in schema["required"]
|
||||
assert "variables" in schema["required"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_variables(
|
||||
document_state_tool,
|
||||
mock_session_manager
|
||||
):
|
||||
"""Test documenting variables in a session."""
|
||||
arguments = {
|
||||
"session_id": "test-session",
|
||||
"variables": {
|
||||
"df": "Customer data with 1000 rows",
|
||||
"model": "Trained RandomForest classifier"
|
||||
}
|
||||
}
|
||||
|
||||
result = await document_state_tool.execute(arguments)
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TextContent)
|
||||
|
||||
# Parse JSON response
|
||||
response = json.loads(result[0].text)
|
||||
assert response["success"] is True
|
||||
assert response["documented_count"] == 2
|
||||
assert response["session_id"] == "test-session"
|
||||
|
||||
# Verify session manager was called
|
||||
mock_session_manager.document_variables.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_with_note(
|
||||
document_state_tool,
|
||||
mock_session_manager
|
||||
):
|
||||
"""Test documenting with a note."""
|
||||
arguments = {
|
||||
"session_id": "test-session",
|
||||
"variables": {
|
||||
"result": "Final analysis output"
|
||||
},
|
||||
"note": "Analysis complete, ready for reporting"
|
||||
}
|
||||
|
||||
result = await document_state_tool.execute(arguments)
|
||||
|
||||
# Parse JSON response
|
||||
response = json.loads(result[0].text)
|
||||
assert response["success"] is True
|
||||
|
||||
# Verify note was passed
|
||||
call_args = mock_session_manager.document_variables.call_args
|
||||
assert call_args is not None
|
||||
assert call_args.kwargs["note"] == "Analysis complete, ready for reporting"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clear_existing_documentation(
|
||||
document_state_tool,
|
||||
mock_session_manager
|
||||
):
|
||||
"""Test clearing existing documentation."""
|
||||
arguments = {
|
||||
"session_id": "test-session",
|
||||
"variables": {
|
||||
"new_var": "New variable"
|
||||
},
|
||||
"clear": True
|
||||
}
|
||||
|
||||
result = await document_state_tool.execute(arguments)
|
||||
|
||||
# Parse JSON response
|
||||
response = json.loads(result[0].text)
|
||||
assert response["success"] is True
|
||||
|
||||
# Verify clear flag was passed
|
||||
call_args = mock_session_manager.document_variables.call_args
|
||||
assert call_args is not None
|
||||
assert call_args.kwargs["clear"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_validation(
|
||||
document_state_tool,
|
||||
mock_session_manager
|
||||
):
|
||||
"""Test that validation fails for non-existent session."""
|
||||
mock_session_manager.session_exists = Mock(return_value=False)
|
||||
|
||||
arguments = {
|
||||
"session_id": "nonexistent",
|
||||
"variables": {
|
||||
"x": "test"
|
||||
}
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="Session"):
|
||||
await document_state_tool.execute(arguments)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_arguments_missing_session_id(document_state_tool):
|
||||
"""Test that validation fails when session_id is missing."""
|
||||
arguments = {
|
||||
"variables": {"x": "test"}
|
||||
}
|
||||
|
||||
with pytest.raises((ValueError, KeyError)):
|
||||
await document_state_tool.execute(arguments)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_arguments_missing_variables(document_state_tool):
|
||||
"""Test that validation fails when variables is missing."""
|
||||
arguments = {
|
||||
"session_id": "test-session"
|
||||
}
|
||||
|
||||
with pytest.raises((ValueError, KeyError)):
|
||||
await document_state_tool.execute(arguments)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_arguments_invalid_variables_type(document_state_tool):
|
||||
"""Test that validation fails when variables is not a dict."""
|
||||
arguments = {
|
||||
"session_id": "test-session",
|
||||
"variables": ["not", "a", "dict"]
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="variables"):
|
||||
await document_state_tool.execute(arguments)
|
||||
389
tests/server/tools/test_execute_python.py
Normal file
389
tests/server/tools/test_execute_python.py
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
"""Tests for Execute Python Tool."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock, patch
|
||||
from mcp.types import Tool, TextContent
|
||||
import json
|
||||
|
||||
from mcp_forge.server.tools.execute_python import ExecutePythonTool
|
||||
from mcp_forge.execution.simple.backend import ExecutionResult
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_simple_backend():
|
||||
"""Mock simple backend."""
|
||||
backend = Mock()
|
||||
backend.execute = AsyncMock(return_value=ExecutionResult(
|
||||
success=True,
|
||||
stdout="Hello World",
|
||||
stderr="",
|
||||
result="42",
|
||||
execution_time=0.5,
|
||||
exit_code=0
|
||||
))
|
||||
return backend
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_jupyter_backend():
|
||||
"""Mock jupyter backend."""
|
||||
backend = Mock()
|
||||
backend.execute = AsyncMock(return_value=ExecutionResult(
|
||||
success=True,
|
||||
stdout="Stateful execution",
|
||||
stderr="",
|
||||
result="session-123",
|
||||
execution_time=1.2,
|
||||
exit_code=0
|
||||
))
|
||||
backend.execute_in_session = AsyncMock(return_value=ExecutionResult(
|
||||
success=True,
|
||||
stdout="Using existing session",
|
||||
stderr="",
|
||||
result="[1, 2, 3]",
|
||||
execution_time=0.3,
|
||||
exit_code=0
|
||||
))
|
||||
return backend
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client_manager():
|
||||
"""Mock MCP client manager."""
|
||||
manager = Mock()
|
||||
manager.list_all_tools = AsyncMock(return_value=[
|
||||
{"name": "github_search_repos", "description": "Search GitHub repositories"},
|
||||
{"name": "filesystem_read", "description": "Read file contents"}
|
||||
])
|
||||
return manager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_bridge_server():
|
||||
"""Mock tool bridge server."""
|
||||
server = Mock()
|
||||
server.socket_path = "/tmp/mcp-bridge.sock"
|
||||
server.is_running = Mock(return_value=True)
|
||||
return server
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_injection_generator():
|
||||
"""Mock tool injection generator."""
|
||||
generator = Mock()
|
||||
generator.generate_injection_code = Mock(return_value="""
|
||||
# MCP Tool Injection
|
||||
def github_search_repos(**kwargs):
|
||||
import socket
|
||||
# ... tool implementation
|
||||
pass
|
||||
""")
|
||||
return generator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config():
|
||||
"""Mock forge configuration."""
|
||||
config = Mock()
|
||||
config.execution = Mock()
|
||||
config.execution.default_backend = "simple"
|
||||
config.execution.default_timeout = 300
|
||||
config.execution.max_timeout = 1800
|
||||
config.execution.default_memory = "512m"
|
||||
config.execution.max_memory = "2g"
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def execute_python_tool(
|
||||
mock_simple_backend,
|
||||
mock_jupyter_backend,
|
||||
mock_client_manager,
|
||||
mock_bridge_server,
|
||||
mock_injection_generator,
|
||||
mock_config
|
||||
):
|
||||
"""Create ExecutePythonTool instance with mocked dependencies."""
|
||||
return ExecutePythonTool(
|
||||
simple_backend=mock_simple_backend,
|
||||
jupyter_backend=mock_jupyter_backend,
|
||||
client_manager=mock_client_manager,
|
||||
bridge_server=mock_bridge_server,
|
||||
injection_generator=mock_injection_generator,
|
||||
config=mock_config
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_tool_definition(execute_python_tool):
|
||||
"""Test that tool definition matches MCP spec."""
|
||||
definition = execute_python_tool.get_tool_definition()
|
||||
|
||||
assert isinstance(definition, Tool)
|
||||
assert definition.name == "execute_python"
|
||||
assert definition.description is not None
|
||||
assert "Execute Python code" in definition.description
|
||||
|
||||
# Verify required schema properties
|
||||
schema = definition.inputSchema
|
||||
assert schema["type"] == "object"
|
||||
assert "code" in schema["properties"]
|
||||
assert "mcp_tools" in schema["properties"]
|
||||
assert "session_id" in schema["properties"]
|
||||
assert "backend" in schema["properties"]
|
||||
assert "timeout" in schema["properties"]
|
||||
assert "custom_image" in schema["properties"]
|
||||
assert "environment" in schema["properties"]
|
||||
assert schema["required"] == ["code"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_simple_backend_stateless(
|
||||
execute_python_tool,
|
||||
mock_simple_backend
|
||||
):
|
||||
"""Test execution with simple backend (stateless)."""
|
||||
arguments = {
|
||||
"code": "print('Hello World'); 42"
|
||||
}
|
||||
|
||||
result = await execute_python_tool.execute(arguments)
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TextContent)
|
||||
|
||||
# Parse JSON response
|
||||
response = json.loads(result[0].text)
|
||||
assert response["success"] is True
|
||||
assert response["stdout"] == "Hello World"
|
||||
assert response["result"] == "42"
|
||||
assert response["execution_time"] == 0.5
|
||||
assert "session_id" not in response or response["session_id"] is None
|
||||
|
||||
# Verify simple backend was called
|
||||
mock_simple_backend.execute.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_jupyter_backend_stateful(
|
||||
execute_python_tool,
|
||||
mock_jupyter_backend
|
||||
):
|
||||
"""Test execution with jupyter backend (stateful)."""
|
||||
arguments = {
|
||||
"code": "x = 42; print('Stateful')",
|
||||
"session_id": "test-session",
|
||||
"backend": "jupyter"
|
||||
}
|
||||
|
||||
result = await execute_python_tool.execute(arguments)
|
||||
|
||||
# Parse JSON response
|
||||
response = json.loads(result[0].text)
|
||||
assert response["success"] is True
|
||||
assert "session_id" in response
|
||||
|
||||
# Verify jupyter backend was called
|
||||
mock_jupyter_backend.execute_in_session.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_with_mcp_tools(
|
||||
execute_python_tool,
|
||||
mock_simple_backend,
|
||||
mock_injection_generator,
|
||||
mock_bridge_server
|
||||
):
|
||||
"""Test execution with MCP tool injection."""
|
||||
arguments = {
|
||||
"code": "repos = github_search_repos(query='test', max_results=10)",
|
||||
"mcp_tools": ["github_search_repos"]
|
||||
}
|
||||
|
||||
result = await execute_python_tool.execute(arguments)
|
||||
|
||||
# Parse JSON response
|
||||
response = json.loads(result[0].text)
|
||||
assert response["success"] is True
|
||||
assert "available_tools" in response
|
||||
assert "github_search_repos" in response["available_tools"]
|
||||
|
||||
# Verify injection generator was called
|
||||
mock_injection_generator.generate_injection_code.assert_called_once_with(
|
||||
tool_names=["github_search_repos"],
|
||||
socket_path="/tmp/mcp-bridge.sock"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_with_custom_image(
|
||||
execute_python_tool,
|
||||
mock_simple_backend
|
||||
):
|
||||
"""Test execution with custom image."""
|
||||
arguments = {
|
||||
"code": "import pandas as pd; pd.DataFrame()",
|
||||
"custom_image": "mcp-forge/custom:my-ml-env"
|
||||
}
|
||||
|
||||
_result = await execute_python_tool.execute(arguments)
|
||||
|
||||
# Verify backend was called with custom image
|
||||
call_args = mock_simple_backend.execute.call_args
|
||||
assert call_args is not None
|
||||
# Check that custom_image was passed in execution options
|
||||
assert "image" in call_args.kwargs or "custom_image" in call_args.kwargs
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_with_environment_template(
|
||||
execute_python_tool,
|
||||
mock_simple_backend
|
||||
):
|
||||
"""Test execution with environment template."""
|
||||
arguments = {
|
||||
"code": "import numpy as np; np.array([1,2,3])",
|
||||
"environment": "datascience"
|
||||
}
|
||||
|
||||
_result = await execute_python_tool.execute(arguments)
|
||||
|
||||
# Verify backend was called with environment specification
|
||||
mock_simple_backend.execute.assert_called_once()
|
||||
call_args = mock_simple_backend.execute.call_args
|
||||
assert call_args is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_with_timeout(
|
||||
execute_python_tool,
|
||||
mock_simple_backend
|
||||
):
|
||||
"""Test execution with custom timeout."""
|
||||
arguments = {
|
||||
"code": "import time; time.sleep(10)",
|
||||
"timeout": 5
|
||||
}
|
||||
|
||||
_result = await execute_python_tool.execute(arguments)
|
||||
|
||||
# Verify timeout was passed to backend
|
||||
call_args = mock_simple_backend.execute.call_args
|
||||
assert call_args is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_arguments_missing_code(execute_python_tool):
|
||||
"""Test that validation fails when code is missing."""
|
||||
arguments = {}
|
||||
|
||||
with pytest.raises((ValueError, KeyError)):
|
||||
await execute_python_tool.execute(arguments)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_arguments_invalid_backend(execute_python_tool):
|
||||
"""Test that validation fails for invalid backend."""
|
||||
arguments = {
|
||||
"code": "print('test')",
|
||||
"backend": "invalid"
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="backend"):
|
||||
await execute_python_tool.execute(arguments)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_arguments_invalid_timeout(
|
||||
execute_python_tool,
|
||||
mock_config
|
||||
):
|
||||
"""Test that validation fails for timeout exceeding max."""
|
||||
arguments = {
|
||||
"code": "print('test')",
|
||||
"timeout": 3600 # Exceeds max_timeout of 1800
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="Timeout"):
|
||||
await execute_python_tool.execute(arguments)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_backend_selection_with_session_id(execute_python_tool):
|
||||
"""Test that jupyter backend is selected when session_id provided."""
|
||||
backend = execute_python_tool._select_backend(
|
||||
session_id="test-session",
|
||||
backend=None
|
||||
)
|
||||
|
||||
assert backend == "jupyter"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_backend_selection_default(
|
||||
execute_python_tool,
|
||||
mock_config
|
||||
):
|
||||
"""Test that default backend is used when no session_id."""
|
||||
backend = execute_python_tool._select_backend(
|
||||
session_id=None,
|
||||
backend=None
|
||||
)
|
||||
|
||||
assert backend == "simple"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_backend_selection_explicit(execute_python_tool):
|
||||
"""Test that explicit backend is respected."""
|
||||
backend = execute_python_tool._select_backend(
|
||||
session_id=None,
|
||||
backend="jupyter"
|
||||
)
|
||||
|
||||
assert backend == "jupyter"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execution_error_handling(
|
||||
execute_python_tool,
|
||||
mock_simple_backend
|
||||
):
|
||||
"""Test that execution errors are handled gracefully."""
|
||||
mock_simple_backend.execute = AsyncMock(return_value=ExecutionResult(
|
||||
success=False,
|
||||
stdout="",
|
||||
stderr="NameError: name 'undefined_variable' is not defined",
|
||||
result=None,
|
||||
execution_time=0.1,
|
||||
exit_code=1
|
||||
))
|
||||
|
||||
arguments = {
|
||||
"code": "print(undefined_variable)"
|
||||
}
|
||||
|
||||
result = await execute_python_tool.execute(arguments)
|
||||
|
||||
# Parse JSON response
|
||||
response = json.loads(result[0].text)
|
||||
assert response["success"] is False
|
||||
assert "NameError" in response["stderr"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bridge_server_not_running(
|
||||
execute_python_tool,
|
||||
mock_bridge_server
|
||||
):
|
||||
"""Test that error is raised if bridge server not running."""
|
||||
mock_bridge_server.is_running = Mock(return_value=False)
|
||||
|
||||
arguments = {
|
||||
"code": "repos = github_search_repos(query='test')",
|
||||
"mcp_tools": ["github_search_repos"]
|
||||
}
|
||||
|
||||
with pytest.raises(RuntimeError, match="bridge server"):
|
||||
await execute_python_tool.execute(arguments)
|
||||
Loading…
Add table
Add a link
Reference in a new issue