initial commit after one day coding agent session

This commit is contained in:
Hans Aschauer 2026-02-07 07:45:57 +01:00
commit 372af75b90
88 changed files with 22694 additions and 0 deletions

View 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)