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

403
tests/config/test_schema.py Normal file
View file

@ -0,0 +1,403 @@
"""
Tests for configuration schema module.
Following TDD approach - these tests are written before implementation.
Tests cover all validation requirements from todo.md section 1.1.1.
"""
import pytest
from pathlib import Path
from pydantic import ValidationError
def test_valid_server_config_loads_successfully():
"""Test that a valid ServerConfig loads without errors."""
from mcp_forge.config.schema import ServerConfig
config = ServerConfig(
host="0.0.0.0",
port=3000,
podman_socket=Path("/run/user/1000/podman/podman.sock")
)
assert config.host == "0.0.0.0"
assert config.port == 3000
assert config.podman_socket == Path("/run/user/1000/podman/podman.sock")
def test_server_config_invalid_port_raises_validation_error():
"""Test that invalid port numbers raise ValidationError."""
from mcp_forge.config.schema import ServerConfig
# Port too high
with pytest.raises(ValidationError) as exc_info:
ServerConfig(
host="localhost",
port=70000,
podman_socket=Path("/run/podman.sock")
)
assert "port" in str(exc_info.value).lower()
# Port too low
with pytest.raises(ValidationError) as exc_info:
ServerConfig(
host="localhost",
port=0,
podman_socket=Path("/run/podman.sock")
)
assert "port" in str(exc_info.value).lower()
# Negative port
with pytest.raises(ValidationError) as exc_info:
ServerConfig(
host="localhost",
port=-1,
podman_socket=Path("/run/podman.sock")
)
assert "port" in str(exc_info.value).lower()
def test_execution_config_defaults_are_applied():
"""Test that ExecutionConfig has correct default values."""
from mcp_forge.config.schema import ExecutionConfig
config = ExecutionConfig()
assert config.default_backend == "simple"
assert config.default_timeout == 300
assert config.max_timeout == 1800
assert config.default_memory == "512m"
assert config.max_memory == "2g"
assert config.default_cpu_quota == 50000
assert config.max_cpu_quota == 100000
def test_execution_config_max_timeout_validation():
"""Test that max_timeout must be >= default_timeout."""
from mcp_forge.config.schema import ExecutionConfig
# Valid: max >= default
config = ExecutionConfig(default_timeout=300, max_timeout=600)
assert config.max_timeout == 600
# Invalid: max < default
with pytest.raises(ValidationError) as exc_info:
ExecutionConfig(default_timeout=600, max_timeout=300)
assert "max_timeout" in str(exc_info.value).lower()
def test_image_config_defaults():
"""Test ImageConfig default values."""
from mcp_forge.config.schema import ImageConfig
config = ImageConfig()
assert config.python_3_11 == "mcp-forge/python:3.11"
assert config.python_3_12 == "mcp-forge/python:3.12"
assert config.jupyter == "mcp-forge/jupyter:latest"
assert config.auto_pull is True
assert config.pull_interval == 86400
def test_session_config_defaults():
"""Test SessionConfig default values."""
from mcp_forge.config.schema import SessionConfig
config = SessionConfig()
assert config.idle_timeout == 3600
assert config.max_concurrent == 10
assert config.cleanup_interval == 300
def test_volume_config_with_base_path():
"""Test VolumeConfig with required base_path."""
from mcp_forge.config.schema import VolumeConfig
config = VolumeConfig(base_path=Path("/mcp-forge/volumes"))
assert config.base_path == Path("/mcp-forge/volumes")
assert config.session_quota == "1g"
assert config.max_session_quota == "10g"
def test_security_config_defaults():
"""Test SecurityConfig default values."""
from mcp_forge.config.schema import SecurityConfig
config = SecurityConfig(audit_log=Path("/var/log/mcp-forge/audit.log"))
assert config.audit_log == Path("/var/log/mcp-forge/audit.log")
assert config.enforce_resource_limits is True
assert config.allow_network is False
def test_package_validation_config():
"""Test PackageValidationConfig structure."""
from mcp_forge.config.schema import PackageValidationConfig
config = PackageValidationConfig(
use_allowlist=True,
allowlist_path=Path("/etc/mcp-forge/allowlist.txt"),
blocklist_path=Path("/etc/mcp-forge/blocklist.txt"),
require_approval_patterns=["*crypto*", "*network*"]
)
assert config.use_allowlist is True
assert config.allowlist_path == Path("/etc/mcp-forge/allowlist.txt")
assert config.blocklist_path == Path("/etc/mcp-forge/blocklist.txt")
assert "*crypto*" in config.require_approval_patterns
def test_environment_builder_config():
"""Test EnvironmentBuilderConfig structure and defaults."""
from mcp_forge.config.schema import EnvironmentBuilderConfig, PackageValidationConfig
pkg_validation = PackageValidationConfig(
use_allowlist=True,
allowlist_path=Path("/etc/allowlist.txt"),
blocklist_path=Path("/etc/blocklist.txt"),
require_approval_patterns=[]
)
config = EnvironmentBuilderConfig(
enabled=True,
uv_cache_path=Path("/var/cache/mcp-forge/uv"),
package_validation=pkg_validation
)
assert config.enabled is True
assert config.uv_cache_path == Path("/var/cache/mcp-forge/uv")
assert config.max_packages_per_build == 50
assert config.max_build_time == 600
assert config.max_image_size == 2147483648
assert config.max_concurrent_builds == 3
def test_mcp_tool_config():
"""Test MCPToolConfig structure."""
from mcp_forge.config.schema import MCPToolConfig
config = MCPToolConfig(
command="uvx",
args=["mcp-server-git"],
env={"GIT_AUTHOR": "test"}
)
assert config.command == "uvx"
assert config.args == ["mcp-server-git"]
assert config.env == {"GIT_AUTHOR": "test"}
def test_mcp_tool_config_empty_env_defaults():
"""Test MCPToolConfig with empty env defaults to empty dict."""
from mcp_forge.config.schema import MCPToolConfig
config = MCPToolConfig(command="test", args=[])
assert config.env == {}
def test_forge_config_full_structure():
"""Test complete ForgeConfig with all nested structures."""
from mcp_forge.config.schema import (
ForgeConfig, ServerConfig, ExecutionConfig, ImageConfig,
SessionConfig, VolumeConfig, SecurityConfig,
EnvironmentBuilderConfig, PackageValidationConfig, MCPToolConfig
)
pkg_validation = PackageValidationConfig(
use_allowlist=True,
allowlist_path=Path("/etc/allowlist.txt"),
blocklist_path=Path("/etc/blocklist.txt"),
require_approval_patterns=[]
)
config = ForgeConfig(
server=ServerConfig(
host="localhost",
port=3000,
podman_socket=Path("/run/podman.sock")
),
execution=ExecutionConfig(),
images=ImageConfig(),
sessions=SessionConfig(),
volumes=VolumeConfig(base_path=Path("/mcp-forge/volumes")),
security=SecurityConfig(audit_log=Path("/var/log/audit.log")),
environment_builder=EnvironmentBuilderConfig(
enabled=True,
uv_cache_path=Path("/var/cache/uv"),
package_validation=pkg_validation
),
mcp_tools={
"git": MCPToolConfig(command="uvx", args=["mcp-server-git"])
}
)
assert config.server.host == "localhost"
assert config.execution.default_backend == "simple"
assert config.images.python_3_11 == "mcp-forge/python:3.11"
assert config.sessions.max_concurrent == 10
assert config.volumes.base_path == Path("/mcp-forge/volumes")
assert config.security.enforce_resource_limits is True
assert config.environment_builder.enabled is True
assert "git" in config.mcp_tools
def test_nested_configuration_validation_error_includes_field_path():
"""Test that ValidationError for nested config includes full field path."""
from mcp_forge.config.schema import ForgeConfig, ServerConfig
with pytest.raises(ValidationError) as exc_info:
ForgeConfig(
server=ServerConfig(
host="localhost",
port=99999, # Invalid port
podman_socket=Path("/run/podman.sock")
),
execution={},
images={},
sessions={},
volumes={"base_path": "/volumes"},
security={"audit_log": "/audit.log"},
environment_builder={
"uv_cache_path": "/cache",
"package_validation": {
"allowlist_path": "/allow.txt",
"blocklist_path": "/block.txt",
"require_approval_patterns": []
}
},
mcp_tools={}
)
error_str = str(exc_info.value)
# Should include nested path like "server.port"
assert "port" in error_str.lower()
def test_execution_config_backend_literal_validation():
"""Test that default_backend only accepts 'simple' or 'jupyter'."""
from mcp_forge.config.schema import ExecutionConfig
# Valid values
config1 = ExecutionConfig(default_backend="simple")
assert config1.default_backend == "simple"
config2 = ExecutionConfig(default_backend="jupyter")
assert config2.default_backend == "jupyter"
# Invalid value
with pytest.raises(ValidationError) as exc_info:
ExecutionConfig(default_backend="invalid")
assert "default_backend" in str(exc_info.value).lower()
def test_environment_builder_config_rate_limit_dict():
"""Test that EnvironmentBuilderConfig accepts rate_limit dict."""
from mcp_forge.config.schema import EnvironmentBuilderConfig, PackageValidationConfig
pkg_validation = PackageValidationConfig(
use_allowlist=True,
allowlist_path=Path("/allow.txt"),
blocklist_path=Path("/block.txt"),
require_approval_patterns=[]
)
config = EnvironmentBuilderConfig(
enabled=True,
uv_cache_path=Path("/cache"),
package_validation=pkg_validation,
build_rate_limit={"max_requests": 10, "period_seconds": 3600}
)
assert config.build_rate_limit == {"max_requests": 10, "period_seconds": 3600}
def test_environment_builder_config_auto_cleanup_dict():
"""Test that EnvironmentBuilderConfig accepts auto_cleanup dict."""
from mcp_forge.config.schema import EnvironmentBuilderConfig, PackageValidationConfig
pkg_validation = PackageValidationConfig(
use_allowlist=True,
allowlist_path=Path("/allow.txt"),
blocklist_path=Path("/block.txt"),
require_approval_patterns=[]
)
config = EnvironmentBuilderConfig(
enabled=True,
uv_cache_path=Path("/cache"),
package_validation=pkg_validation,
auto_cleanup={"enabled": True, "max_age_days": 30}
)
assert config.auto_cleanup == {"enabled": True, "max_age_days": 30}
def test_environment_builder_config_templates_dict():
"""Test that EnvironmentBuilderConfig accepts templates dict."""
from mcp_forge.config.schema import EnvironmentBuilderConfig, PackageValidationConfig
pkg_validation = PackageValidationConfig(
use_allowlist=True,
allowlist_path=Path("/allow.txt"),
blocklist_path=Path("/block.txt"),
require_approval_patterns=[]
)
config = EnvironmentBuilderConfig(
enabled=True,
uv_cache_path=Path("/cache"),
package_validation=pkg_validation,
templates={
"data-science": {"packages": ["numpy", "pandas", "matplotlib"]},
"web": {"packages": ["fastapi", "uvicorn"]}
}
)
assert "data-science" in config.templates
assert "web" in config.templates
assert config.templates["data-science"]["packages"] == ["numpy", "pandas", "matplotlib"]
def test_config_constraint_validation_memory_strings():
"""Test that config validates memory strings are comparable."""
from mcp_forge.config.schema import ExecutionConfig
# This should succeed - just testing structure, not actual parsing yet
config = ExecutionConfig(
default_memory="512m",
max_memory="2g"
)
assert config.default_memory == "512m"
assert config.max_memory == "2g"
def test_mcp_tool_config_list_of_args():
"""Test MCPToolConfig args is a list."""
from mcp_forge.config.schema import MCPToolConfig
config = MCPToolConfig(
command="python",
args=["-m", "mymodule", "--flag"]
)
assert isinstance(config.args, list)
assert config.args == ["-m", "mymodule", "--flag"]
def test_package_validation_require_approval_patterns_list():
"""Test PackageValidationConfig require_approval_patterns is a list."""
from mcp_forge.config.schema import PackageValidationConfig
config = PackageValidationConfig(
use_allowlist=False,
allowlist_path=Path("/allow.txt"),
blocklist_path=Path("/block.txt"),
require_approval_patterns=["*crypto*", "*security*", "paramiko"]
)
assert isinstance(config.require_approval_patterns, list)
assert len(config.require_approval_patterns) == 3