Adapt tests for pod_executor package and clean up mcp_forge

- Created tests/pod_executor/ with adapted tests from old locations
- tests/pod_executor/simple/test_executor.py: 17/17 tests passing
- tests/pod_executor/security/test_resource_limits.py: 22/23 tests passing

- Removed old test locations (will be deleted with mcp_forge cleanup)
- Fixed all corrupted files from sed/quote issues using Python scripts
- Removed mcp_forge dependencies from pod_executor:
  - Removed ForgeConfig from backend.py (explicit parameters)
  - Removed SessionConfig from sessions.py (explicit parameters)
  - Fixed all audit logger calls to use string-based events

- Updated mcp_forge/security/__init__.py:
  - Removed resource_limits imports (now in pod_executor)
  - Added comment directing to pod_executor.security.resource_limits

- Deleted from mcp_forge:
  - src/mcp_forge/execution/ (simple and jupyter backends)
  - src/mcp_forge/podman/ (container management)
  - src/mcp_forge/security/resource_limits.py

Total: 39/40 tests passing in pod_executor package
This commit is contained in:
Hans Aschauer 2026-02-07 11:05:32 +01:00
parent 63d9b55a00
commit 3a6bd01272
25 changed files with 1390 additions and 3166 deletions

View file

@ -0,0 +1 @@
"""Tests for pod_executor package."""

View file

@ -0,0 +1 @@
"""Tests for pod_executor.security module."""

View file

@ -0,0 +1,275 @@
"""
Tests for pod_executor resource limits module.
Tests cover all parsing and validation requirements.
"""
import pytest
def test_parse_memory_string_megabytes():
"""Test parsing memory string with megabytes suffix."""
from pod_executor.security.resource_limits import parse_memory_string
result = parse_memory_string("512m")
assert result == 536870912 # 512 * 1024 * 1024
def test_parse_memory_string_gigabytes():
"""Test parsing memory string with gigabytes suffix."""
from pod_executor.security.resource_limits import parse_memory_string
result = parse_memory_string("2g")
assert result == 2147483648 # 2 * 1024 * 1024 * 1024
def test_parse_memory_string_kilobytes():
"""Test parsing memory string with kilobytes suffix."""
from pod_executor.security.resource_limits import parse_memory_string
result = parse_memory_string("1024k")
assert result == 1048576 # 1024 * 1024
def test_parse_memory_string_case_insensitive():
"""Test that memory string parsing is case-insensitive."""
from pod_executor.security.resource_limits import parse_memory_string
assert parse_memory_string("512M") == 536870912
assert parse_memory_string("2G") == 2147483648
assert parse_memory_string("1024K") == 1048576
def test_parse_memory_string_invalid_format_raises_value_error():
"""Test that invalid format raises ValueError."""
from pod_executor.security.resource_limits import parse_memory_string
with pytest.raises(ValueError) as exc_info:
parse_memory_string("invalid")
assert "invalid" in str(exc_info.value).lower() or "format" in str(exc_info.value).lower()
with pytest.raises(ValueError):
parse_memory_string("512x") # Invalid suffix
with pytest.raises(ValueError):
parse_memory_string("abc") # Not a number
def test_parse_memory_string_negative_value_raises_value_error():
"""Test that negative values raise ValueError."""
from pod_executor.security.resource_limits import parse_memory_string
with pytest.raises(ValueError) as exc_info:
parse_memory_string("-512m")
assert "positive" in str(exc_info.value).lower() or "negative" in str(exc_info.value).lower()
def test_parse_memory_string_zero_value_raises_value_error():
"""Test that zero value raises ValueError."""
from pod_executor.security.resource_limits import parse_memory_string
with pytest.raises(ValueError) as exc_info:
parse_memory_string("0m")
assert "positive" in str(exc_info.value).lower() or "zero" in str(exc_info.value).lower()
def test_parse_cpu_quota_valid_value():
"""Test that valid CPU quota values are accepted."""
from pod_executor.security.resource_limits import parse_cpu_quota
result = parse_cpu_quota(50000)
assert result == 50000
result = parse_cpu_quota(100000) # 100% of one core
assert result == 100000
def test_parse_cpu_quota_max_limit():
"""Test that CPU quota has a reasonable maximum (10 cores)."""
from pod_executor.security.resource_limits import parse_cpu_quota
# Should accept up to 1000000 (10 cores)
result = parse_cpu_quota(1000000)
assert result == 1000000
# Should reject more than 10 cores
with pytest.raises(ValueError) as exc_info:
parse_cpu_quota(1000001)
assert "1000000" in str(exc_info.value) or "maximum" in str(exc_info.value).lower()
def test_parse_cpu_quota_negative_raises_value_error():
"""Test that negative CPU quota raises ValueError."""
from pod_executor.security.resource_limits import parse_cpu_quota
with pytest.raises(ValueError) as exc_info:
parse_cpu_quota(-1)
assert "positive" in str(exc_info.value).lower() or "negative" in str(exc_info.value).lower()
def test_parse_cpu_quota_zero_raises_value_error():
"""Test that zero CPU quota raises ValueError."""
from pod_executor.security.resource_limits import parse_cpu_quota
with pytest.raises(ValueError) as exc_info:
parse_cpu_quota(0)
assert "positive" in str(exc_info.value).lower() or "zero" in str(exc_info.value).lower()
def test_parse_storage_string_same_as_memory():
"""Test that storage parsing works the same as memory parsing."""
from pod_executor.security.resource_limits import parse_storage_string
assert parse_storage_string("1g") == 1073741824
assert parse_storage_string("512m") == 536870912
assert parse_storage_string("2048k") == 2097152
def test_resource_limits_class_initialization():
"""Test ResourceLimits class initializes correctly."""
from pod_executor.security.resource_limits import ResourceLimits
limits = ResourceLimits(
memory="512m",
storage="1g",
cpu_quota=50000,
timeout=300
)
assert limits.memory_bytes == 536870912
assert limits.storage_bytes == 1073741824
assert limits.cpu_quota == 50000
assert limits.timeout == 300
def test_resource_limits_validates_memory():
"""Test that ResourceLimits validates memory string."""
from pod_executor.security.resource_limits import ResourceLimits
with pytest.raises(ValueError):
ResourceLimits(
memory="invalid",
storage="1g",
cpu_quota=50000,
timeout=300
)
def test_resource_limits_validates_storage():
"""Test that ResourceLimits validates storage string."""
from pod_executor.security.resource_limits import ResourceLimits
with pytest.raises(ValueError):
ResourceLimits(
memory="512m",
storage="invalid",
cpu_quota=50000,
timeout=300
)
def test_resource_limits_validates_cpu_quota():
"""Test that ResourceLimits validates CPU quota."""
from pod_executor.security.resource_limits import ResourceLimits
with pytest.raises(ValueError):
ResourceLimits(
memory="512m",
storage="1g",
cpu_quota=-1,
timeout=300
)
def test_resource_limits_to_podman_params():
"""Test conversion to Podman container parameters."""
from pod_executor.security.resource_limits import ResourceLimits
limits = ResourceLimits(
memory="512m",
storage="1g",
cpu_quota=50000,
timeout=300
)
params = limits.to_podman_params()
assert isinstance(params, dict)
assert "mem_limit" in params
assert params["mem_limit"] == "536870912" # Should be string for Podman
# CPU quota is set via cpu_quota parameter
assert "cpu_quota" in params
assert params["cpu_quota"] == 50000
def test_resource_limits_default_timeout():
"""Test that ResourceLimits has a default timeout."""
from pod_executor.security.resource_limits import ResourceLimits
limits = ResourceLimits(
memory="512m",
storage="1g",
cpu_quota=50000
)
assert limits.timeout == 300 # Default from signature
def test_parse_memory_string_with_spaces():
"""Test parsing memory strings that have spaces."""
from pod_executor.security.resource_limits import parse_memory_string
# Should handle spaces gracefully (strip them)
result = parse_memory_string(" 512m ")
assert result == 536870912
def test_parse_memory_string_bytes_suffix():
"""Test parsing memory string with bytes suffix (no multiplier)."""
from pod_executor.security.resource_limits import parse_memory_string
# Just a number (bytes) - should this be supported?
# Based on architecture, we support k, m, g suffixes
# Plain numbers should probably raise an error for safety
with pytest.raises(ValueError):
parse_memory_string("1024")
def test_resource_limits_storage_quota_in_podman_params():
"""Test that storage limits are included in Podman params."""
from pod_executor.security.resource_limits import ResourceLimits
limits = ResourceLimits(
memory="512m",
storage="1g",
cpu_quota=50000,
timeout=300
)
params = limits.to_podman_params()
# Storage limit might be set via storage_opt or similar
# The exact parameter depends on Podman API
assert "storage_bytes" in params or "storage_opt" in params
def test_cpu_quota_explanation():
"""Test that CPU quota values have clear meaning."""
from pod_executor.security.resource_limits import parse_cpu_quota
# 100000 = 100% of one CPU core
# 50000 = 50% of one CPU core
# 200000 = 200% = 2 CPU cores
assert parse_cpu_quota(50000) == 50000 # 0.5 cores
assert parse_cpu_quota(100000) == 100000 # 1 core
assert parse_cpu_quota(200000) == 200000 # 2 cores
def test_parse_memory_with_decimal():
"""Test parsing memory strings with decimal values."""
from pod_executor.security.resource_limits import parse_memory_string
# Should handle decimals
result = parse_memory_string("1.5g")
assert result == 1610612736 # 1.5 * 1024 * 1024 * 1024

View file

@ -0,0 +1 @@
"""Tests for pod_executor.simple module."""

View file

@ -0,0 +1,299 @@
"""Tests for the pod_executor Code Executor module."""
import pytest
from unittest.mock import Mock
import json
from pod_executor.simple.executor import CodeExecutor, ExecutionResult
from pod_executor.containers.manager import SecureContainerManager
from pod_executor.security.resource_limits import ResourceLimits
@pytest.fixture
def resource_limits():
"""Standard resource limits for testing."""
return ResourceLimits(
memory="512m",
cpu_quota=50000,
storage="1g",
timeout=30
)
@pytest.fixture
def mock_container_manager():
"""Mock SecureContainerManager."""
manager = Mock(spec=SecureContainerManager)
# Mock container lifecycle
manager.create_container.return_value = "test-container-123"
manager.start_container.return_value = None
manager.stop_container.return_value = None
manager.remove_container.return_value = None
manager.wait_for_container.return_value = 0 # exit code
manager.get_container_logs.return_value = ("", "") # (stdout, stderr)
return manager
@pytest.fixture
def executor(mock_container_manager, resource_limits):
"""CodeExecutor instance with mocked dependencies."""
return CodeExecutor(
container_manager=mock_container_manager,
image="mcp-forge/python:3.11",
resource_limits=resource_limits
)
def test_execute_simple_python_code_returns_result(executor, mock_container_manager):
"""Test executing simple Python code returns the result."""
# Mock successful execution with result
mock_container_manager.get_container_logs.return_value = (
json.dumps({"result": 42, "error": None}),
""
)
result = executor.execute("2 + 2")
assert result.success is True
assert result.result == 42
assert result.exit_code == 0
assert result.error is None
# Verify container lifecycle
mock_container_manager.create_container.assert_called_once()
mock_container_manager.start_container.assert_called_once_with("test-container-123")
mock_container_manager.wait_for_container.assert_called_once_with("test-container-123", timeout=30)
mock_container_manager.remove_container.assert_called_once_with("test-container-123")
def test_execute_code_with_stdout_capture(executor, mock_container_manager):
"""Test code execution captures stdout."""
mock_container_manager.get_container_logs.return_value = (
json.dumps({"result": None, "error": None}) + "\n" + "Hello, World!",
""
)
result = executor.execute('print("Hello, World!")')
assert result.success is True
assert "Hello, World!" in result.stdout
assert result.stderr == ""
def test_execute_code_with_stderr_capture(executor, mock_container_manager):
"""Test code execution captures stderr."""
mock_container_manager.get_container_logs.return_value = (
json.dumps({"result": None, "error": None}),
"Warning: something happened"
)
result = executor.execute('import sys; print("warning", file=sys.stderr)')
assert result.success is True
assert result.stderr == "Warning: something happened"
def test_execute_code_timeout_enforcement(executor, mock_container_manager):
"""Test code execution enforces timeout."""
# Simulate timeout by having wait_for_container take too long
mock_container_manager.wait_for_container.side_effect = TimeoutError("Container exceeded timeout")
result = executor.execute("import time; time.sleep(60)", timeout=1)
assert result.success is False
assert result.error is not None
assert "timeout" in result.error.lower()
# Verify cleanup still happens
mock_container_manager.remove_container.assert_called_once_with("test-container-123")
def test_execute_code_with_exception_handling(executor, mock_container_manager):
"""Test code execution handles exceptions gracefully."""
error_msg = "ZeroDivisionError: division by zero"
mock_container_manager.get_container_logs.return_value = (
json.dumps({"result": None, "error": error_msg}),
""
)
mock_container_manager.wait_for_container.return_value = 1 # non-zero exit
result = executor.execute("1 / 0")
assert result.success is False
assert result.error == error_msg
assert result.exit_code == 1
def test_execute_code_with_syntax_error_returns_clear_error(executor, mock_container_manager):
"""Test code with syntax error returns clear error message."""
error_msg = "SyntaxError: invalid syntax"
mock_container_manager.get_container_logs.return_value = (
json.dumps({"result": None, "error": error_msg}),
""
)
mock_container_manager.wait_for_container.return_value = 1
result = executor.execute("def foo( :")
assert result.success is False
assert "SyntaxError" in result.error
def test_execute_code_with_runtime_error_returns_clear_error(executor, mock_container_manager):
"""Test code with runtime error returns clear error with traceback."""
error_msg = "NameError: name 'undefined_var' is not defined"
mock_container_manager.get_container_logs.return_value = (
json.dumps({"result": None, "error": error_msg}),
""
)
mock_container_manager.wait_for_container.return_value = 1
result = executor.execute("print(undefined_var)")
assert result.success is False
assert "NameError" in result.error
def test_result_serialization_json_compatible_types(executor, mock_container_manager):
"""Test execution result contains only JSON-serializable data."""
mock_container_manager.get_container_logs.return_value = (
json.dumps({"result": [1, 2, {"key": "value"}], "error": None}),
""
)
result = executor.execute('[1, 2, {"key": "value"}]')
# Verify result can be serialized to JSON
result_dict = result.to_dict()
json_str = json.dumps(result_dict)
assert json_str is not None
# Verify result data
assert result.result == [1, 2, {"key": "value"}]
def test_large_output_handling(executor, mock_container_manager):
"""Test execution handles large output without issues."""
large_output = "x" * 10000 # 10KB of output
mock_container_manager.get_container_logs.return_value = (
json.dumps({"result": None, "error": None}) + "\n" + large_output,
""
)
result = executor.execute('print("x" * 10000)')
assert result.success is True
assert len(result.stdout) >= 10000
def test_execution_result_to_dict(resource_limits):
"""Test ExecutionResult.to_dict() returns proper dictionary."""
result = ExecutionResult(
success=True,
stdout="output",
stderr="",
result=42,
execution_time=0.5,
exit_code=0,
error=None
)
result_dict = result.to_dict()
assert isinstance(result_dict, dict)
assert result_dict["success"] is True
assert result_dict["stdout"] == "output"
assert result_dict["stderr"] == ""
assert result_dict["result"] == 42
assert result_dict["execution_time"] == 0.5
assert result_dict["exit_code"] == 0
assert result_dict["error"] is None
def test_prepare_code_wraps_code_properly(executor):
"""Test _prepare_code wraps code to capture result."""
code = "x = 2 + 2\nx"
wrapped = executor._prepare_code(code)
# Wrapped code should be executable Python
assert "import" in wrapped
assert "json" in wrapped
assert code in wrapped or "2 + 2" in wrapped
def test_parse_output_extracts_result_and_error(executor):
"""Test _parse_output correctly extracts result and error from JSON."""
# Test successful result
stdout = json.dumps({"result": 42, "error": None})
result, error = executor._parse_output(stdout)
assert result == 42
assert error is None
# Test error
stdout = json.dumps({"result": None, "error": "ValueError: invalid"})
result, error = executor._parse_output(stdout)
assert result is None
assert error == "ValueError: invalid"
def test_cleanup_happens_even_on_create_failure(executor, mock_container_manager):
"""Test container cleanup happens even if create fails."""
mock_container_manager.create_container.side_effect = Exception("Create failed")
with pytest.raises(Exception, match="Create failed"):
executor.execute("print('test')")
# No container to remove since create failed
mock_container_manager.remove_container.assert_not_called()
def test_cleanup_happens_even_on_start_failure(executor, mock_container_manager):
"""Test container cleanup happens even if start fails."""
mock_container_manager.start_container.side_effect = Exception("Start failed")
with pytest.raises(Exception, match="Start failed"):
executor.execute("print('test')")
# Container should still be removed
mock_container_manager.remove_container.assert_called_once_with("test-container-123")
def test_execution_time_tracking(executor, mock_container_manager):
"""Test execution time is tracked accurately."""
mock_container_manager.get_container_logs.return_value = (
json.dumps({"result": None, "error": None}),
""
)
result = executor.execute("pass")
assert result.execution_time >= 0
assert isinstance(result.execution_time, float)
def test_execute_with_custom_timeout(executor, mock_container_manager):
"""Test execute respects custom timeout parameter."""
mock_container_manager.get_container_logs.return_value = (
json.dumps({"result": None, "error": None}),
""
)
executor.execute("pass", timeout=60)
# Verify wait was called with custom timeout
mock_container_manager.wait_for_container.assert_called_with("test-container-123", timeout=60)
def test_execute_uses_default_timeout_from_resource_limits(executor, mock_container_manager):
"""Test execute uses default timeout from resource limits when not specified."""
mock_container_manager.get_container_logs.return_value = (
json.dumps({"result": None, "error": None}),
""
)
executor.execute("pass") # No timeout specified
# Should use resource_limits.timeout (30)
mock_container_manager.wait_for_container.assert_called_with("test-container-123", timeout=30)