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

439
tests/config/test_loader.py Normal file
View file

@ -0,0 +1,439 @@
"""
Tests for configuration loader module.
Following TDD approach - these tests are written before implementation.
Tests cover all loading and substitution requirements from todo.md section 1.1.2.
"""
import pytest
import yaml
from pathlib import Path
from pydantic import ValidationError
import os
import tempfile
def test_load_from_valid_yaml_file(tmp_path):
"""Test loading configuration from a valid YAML file."""
from mcp_forge.config.loader import load_config
config_file = tmp_path / "config.yaml"
config_content = """
server:
host: "0.0.0.0"
port: 3000
podman_socket: "/run/user/1000/podman/podman.sock"
execution:
default_backend: "simple"
default_timeout: 300
max_timeout: 1800
default_memory: "512m"
max_memory: "2g"
default_cpu_quota: 50000
max_cpu_quota: 100000
images:
python_3_11: "mcp-forge/python:3.11"
python_3_12: "mcp-forge/python:3.12"
jupyter: "mcp-forge/jupyter:latest"
auto_pull: true
pull_interval: 86400
sessions:
idle_timeout: 3600
max_concurrent: 10
cleanup_interval: 300
volumes:
base_path: "/mcp-forge/volumes"
session_quota: "1g"
max_session_quota: "10g"
security:
audit_log: "/var/log/mcp-forge/audit.log"
enforce_resource_limits: true
allow_network: false
environment_builder:
enabled: true
uv_cache_path: "/var/cache/mcp-forge/uv"
max_packages_per_build: 50
max_build_time: 600
max_image_size: 2147483648
max_concurrent_builds: 3
build_rate_limit: {}
auto_cleanup: {}
templates: {}
package_validation:
use_allowlist: true
allowlist_path: "/etc/mcp-forge/allowlist.txt"
blocklist_path: "/etc/mcp-forge/blocklist.txt"
require_approval_patterns: []
mcp_tools:
git:
command: "uvx"
args: ["mcp-server-git"]
env: {}
"""
config_file.write_text(config_content)
config = load_config(config_file)
assert config.server.host == "0.0.0.0"
assert config.server.port == 3000
assert config.execution.default_backend == "simple"
def test_load_from_non_existent_file_raises_file_not_found_error():
"""Test that loading from non-existent file raises FileNotFoundError."""
from mcp_forge.config.loader import load_config
with pytest.raises(FileNotFoundError):
load_config(Path("/non/existent/config.yaml"))
def test_invalid_yaml_raises_yaml_error(tmp_path):
"""Test that invalid YAML syntax raises YAMLError."""
from mcp_forge.config.loader import load_config
config_file = tmp_path / "config.yaml"
config_file.write_text("""
invalid yaml:
- unmatched [bracket
key without value
""")
with pytest.raises(yaml.YAMLError):
load_config(config_file)
def test_environment_variable_substitution_in_strings(tmp_path, monkeypatch):
"""Test that ${VAR} is replaced with environment variable value."""
from mcp_forge.config.loader import load_config
monkeypatch.setenv("TEST_HOST", "test.example.com")
monkeypatch.setenv("TEST_PORT", "4000")
monkeypatch.setenv("PODMAN_SOCKET", "/run/test/podman.sock")
config_file = tmp_path / "config.yaml"
config_content = """
server:
host: "${TEST_HOST}"
port: ${TEST_PORT}
podman_socket: "${PODMAN_SOCKET}"
execution:
default_backend: "simple"
images: {}
sessions: {}
volumes:
base_path: "/volumes"
security:
audit_log: "/audit.log"
environment_builder:
enabled: true
uv_cache_path: "/cache"
package_validation:
allowlist_path: "/allow.txt"
blocklist_path: "/block.txt"
require_approval_patterns: []
mcp_tools: {}
"""
config_file.write_text(config_content)
config = load_config(config_file)
assert config.server.host == "test.example.com"
assert config.server.port == 4000
assert str(config.server.podman_socket) == "/run/test/podman.sock"
def test_nested_environment_variable_substitution(tmp_path, monkeypatch):
"""Test that environment variable substitution works in nested structures."""
from mcp_forge.config.loader import load_config
monkeypatch.setenv("AUDIT_LOG_PATH", "/var/log/audit.log")
monkeypatch.setenv("UV_CACHE", "/var/cache/uv")
config_file = tmp_path / "config.yaml"
config_content = """
server:
host: "localhost"
port: 3000
podman_socket: "/run/podman.sock"
execution: {}
images: {}
sessions: {}
volumes:
base_path: "/volumes"
security:
audit_log: "${AUDIT_LOG_PATH}"
environment_builder:
enabled: true
uv_cache_path: "${UV_CACHE}"
package_validation:
allowlist_path: "/allow.txt"
blocklist_path: "/block.txt"
require_approval_patterns: []
mcp_tools: {}
"""
config_file.write_text(config_content)
config = load_config(config_file)
assert str(config.security.audit_log) == "/var/log/audit.log"
assert str(config.environment_builder.uv_cache_path) == "/var/cache/uv"
def test_missing_environment_variable_raises_clear_error(tmp_path):
"""Test that missing environment variable raises error with variable name."""
from mcp_forge.config.loader import load_config
config_file = tmp_path / "config.yaml"
config_content = """
server:
host: "${MISSING_VAR}"
port: 3000
podman_socket: "/run/podman.sock"
execution: {}
images: {}
sessions: {}
volumes:
base_path: "/volumes"
security:
audit_log: "/audit.log"
environment_builder:
enabled: true
uv_cache_path: "/cache"
package_validation:
allowlist_path: "/allow.txt"
blocklist_path: "/block.txt"
require_approval_patterns: []
mcp_tools: {}
"""
config_file.write_text(config_content)
with pytest.raises(ValueError) as exc_info:
load_config(config_file)
assert "MISSING_VAR" in str(exc_info.value)
def test_load_config_from_dict():
"""Test loading configuration from dictionary (for testing)."""
from mcp_forge.config.loader import load_config_from_dict
config_dict = {
"server": {
"host": "localhost",
"port": 3000,
"podman_socket": "/run/podman.sock"
},
"execution": {},
"images": {},
"sessions": {},
"volumes": {"base_path": "/volumes"},
"security": {"audit_log": "/audit.log"},
"environment_builder": {
"enabled": True,
"uv_cache_path": "/cache",
"package_validation": {
"allowlist_path": "/allow.txt",
"blocklist_path": "/block.txt",
"require_approval_patterns": []
}
},
"mcp_tools": {}
}
config = load_config_from_dict(config_dict)
assert config.server.host == "localhost"
assert config.server.port == 3000
def test_load_config_from_dict_with_env_var_substitution(monkeypatch):
"""Test that load_config_from_dict also performs env var substitution."""
from mcp_forge.config.loader import load_config_from_dict
monkeypatch.setenv("TEST_HOST", "example.com")
config_dict = {
"server": {
"host": "${TEST_HOST}",
"port": 3000,
"podman_socket": "/run/podman.sock"
},
"execution": {},
"images": {},
"sessions": {},
"volumes": {"base_path": "/volumes"},
"security": {"audit_log": "/audit.log"},
"environment_builder": {
"enabled": True,
"uv_cache_path": "/cache",
"package_validation": {
"allowlist_path": "/allow.txt",
"blocklist_path": "/block.txt",
"require_approval_patterns": []
}
},
"mcp_tools": {}
}
config = load_config_from_dict(config_dict)
assert config.server.host == "example.com"
def test_partial_configuration_uses_defaults(tmp_path):
"""Test that missing configuration sections use schema defaults."""
from mcp_forge.config.loader import load_config
config_file = tmp_path / "config.yaml"
config_content = """
server:
host: "localhost"
port: 3000
podman_socket: "/run/podman.sock"
execution: {}
images: {}
sessions: {}
volumes:
base_path: "/volumes"
security:
audit_log: "/audit.log"
environment_builder:
enabled: true
uv_cache_path: "/cache"
package_validation:
allowlist_path: "/allow.txt"
blocklist_path: "/block.txt"
require_approval_patterns: []
mcp_tools: {}
"""
config_file.write_text(config_content)
config = load_config(config_file)
# Check defaults from ExecutionConfig
assert config.execution.default_backend == "simple"
assert config.execution.default_timeout == 300
assert config.execution.max_timeout == 1800
# Check defaults from ImageConfig
assert config.images.python_3_11 == "mcp-forge/python:3.11"
assert config.images.auto_pull is True
def test_substitute_env_vars_recursive():
"""Test that substitute_env_vars works recursively on nested structures."""
from mcp_forge.config.loader import substitute_env_vars
import os
os.environ["TEST_VALUE"] = "substituted"
data = {
"simple": "${TEST_VALUE}",
"nested": {
"deep": "${TEST_VALUE}",
"list": ["${TEST_VALUE}", "plain"]
}
}
result = substitute_env_vars(data)
assert result["simple"] == "substituted"
assert result["nested"]["deep"] == "substituted"
assert result["nested"]["list"][0] == "substituted"
assert result["nested"]["list"][1] == "plain"
def test_substitute_env_vars_with_integer_conversion(monkeypatch):
"""Test that numeric strings in env vars can be converted to integers."""
from mcp_forge.config.loader import substitute_env_vars
monkeypatch.setenv("PORT_NUM", "8080")
data = {
"port": "${PORT_NUM}"
}
result = substitute_env_vars(data)
# Should still be a string after substitution; type conversion handled by Pydantic
assert result["port"] == "8080"
def test_no_eval_or_exec_in_substitution():
"""Test that no eval() or exec() is used - only safe string substitution."""
from mcp_forge.config.loader import substitute_env_vars
import os
# Try to inject malicious code - should be treated as literal string
os.environ["MALICIOUS"] = "__import__('os').system('echo hacked')"
data = {
"value": "${MALICIOUS}"
}
result = substitute_env_vars(data)
# Should be the literal string, not executed
assert result["value"] == "__import__('os').system('echo hacked')"
# And it should NOT have been executed (we can't test side effects easily,
# but the substitution logic shouldn't use eval/exec)
def test_env_var_with_special_characters(monkeypatch):
"""Test that environment variables with special characters are handled correctly."""
from mcp_forge.config.loader import substitute_env_vars
monkeypatch.setenv("SPECIAL_PATH", "/path/with-dashes_and_underscores/123")
data = {
"path": "${SPECIAL_PATH}"
}
result = substitute_env_vars(data)
assert result["path"] == "/path/with-dashes_and_underscores/123"
def test_multiple_env_vars_in_single_string(monkeypatch):
"""Test that multiple environment variables can be substituted in one string."""
from mcp_forge.config.loader import substitute_env_vars
monkeypatch.setenv("BASE_PATH", "/opt/mcp-forge")
monkeypatch.setenv("SUBDIR", "logs")
data = {
"log_path": "${BASE_PATH}/${SUBDIR}/audit.log"
}
result = substitute_env_vars(data)
assert result["log_path"] == "/opt/mcp-forge/logs/audit.log"

View file

@ -0,0 +1,141 @@
"""Tests for MCPToolConfig schema with HTTP/SSE transport."""
import pytest
from pydantic import ValidationError
from mcp_forge.config.schema import MCPToolConfig
def test_stdio_transport_config_valid():
"""Test valid stdio transport configuration."""
config = MCPToolConfig(
transport="stdio",
command="python",
args=["-m", "server"],
env={"KEY": "value"}
)
assert config.transport == "stdio"
assert config.command == "python"
assert config.args == ["-m", "server"]
assert config.env == {"KEY": "value"}
def test_http_transport_config_valid():
"""Test valid HTTP transport configuration."""
config = MCPToolConfig(
transport="http",
url="http://localhost:8006/mcp",
headers={"Authorization": "Bearer token"}
)
assert config.transport == "http"
assert config.url == "http://localhost:8006/mcp"
assert config.headers == {"Authorization": "Bearer token"}
def test_sse_transport_config_valid():
"""Test valid SSE transport configuration."""
config = MCPToolConfig(
transport="sse",
url="http://localhost:9000/events",
headers={"X-Custom": "value"}
)
assert config.transport == "sse"
assert config.url == "http://localhost:9000/events"
assert config.headers == {"X-Custom": "value"}
def test_stdio_without_command_invalid():
"""Test that stdio transport requires command."""
with pytest.raises(ValidationError, match="command is required for stdio transport"):
MCPToolConfig(
transport="stdio",
args=["-m", "server"]
)
def test_http_without_url_invalid():
"""Test that HTTP transport requires URL."""
with pytest.raises(ValidationError, match="url is required for http transport"):
MCPToolConfig(
transport="http",
headers={"Authorization": "Bearer token"}
)
def test_sse_without_url_invalid():
"""Test that SSE transport requires URL."""
with pytest.raises(ValidationError, match="url is required for sse transport"):
MCPToolConfig(
transport="sse",
headers={"X-Custom": "value"}
)
def test_default_transport_is_stdio():
"""Test that default transport is stdio."""
config = MCPToolConfig(
command="python",
args=["-m", "server"]
)
assert config.transport == "stdio"
def test_http_config_with_empty_headers():
"""Test HTTP config with no headers."""
config = MCPToolConfig(
transport="http",
url="http://localhost:8006/mcp"
)
assert config.headers == {}
def test_stdio_config_with_empty_env():
"""Test stdio config with no env vars."""
config = MCPToolConfig(
transport="stdio",
command="python"
)
assert config.env == {}
assert config.args == []
def test_invalid_transport_type():
"""Test that invalid transport type is rejected."""
with pytest.raises(ValidationError):
MCPToolConfig(
transport="invalid",
command="python"
)
def test_stdio_config_minimal():
"""Test minimal stdio config with just command."""
config = MCPToolConfig(
command="python"
)
assert config.transport == "stdio"
assert config.command == "python"
assert config.args == []
assert config.env == {}
def test_http_config_with_multiple_headers():
"""Test HTTP config with multiple headers."""
config = MCPToolConfig(
transport="http",
url="http://localhost:8006/mcp",
headers={
"Authorization": "Bearer token123",
"X-Custom-Header": "value",
"Content-Type": "application/json"
}
)
assert len(config.headers) == 3
assert config.headers["Authorization"] == "Bearer token123"

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