Files
headquarter/apps/api/tests/tools/test_models.py
T
alex 85ae390263 chore: add frontend dependencies and update test fixtures
- Add react-router-dom, @tanstack/react-query, zustand, @headlessui/react
- Update pnpm workspace configuration
- Update test fixtures to reference OpenCode instead of RunFusion
- Add frontend environment variable examples
- Update .gitignore for .opencode and .sisyphus directories
2026-05-14 17:30:41 +02:00

242 lines
8.2 KiB
Python

"""Tests for the tool manifest Pydantic models."""
from __future__ import annotations
import pytest
from pydantic import ValidationError
from app.tools.models import (
HealthCheckConfig,
MountConfig,
PortConfig,
ResourceLimits,
SecretRef,
ToolManifest,
TraefikConfig,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _minimal_manifest(**overrides: object) -> ToolManifest:
defaults: dict[str, object] = {
"id": "test-tool",
"name": "Test Tool",
"image": "test:latest",
"ports": [PortConfig(container_port=8080, primary=True)],
"traefik": TraefikConfig(enabled=False),
}
defaults.update(overrides)
return ToolManifest.model_validate(defaults)
# ---------------------------------------------------------------------------
# Valid construction
# ---------------------------------------------------------------------------
def test_valid_opencode_shape() -> None:
manifest = ToolManifest(
id="opencode",
name="OpenCode",
description="AI-powered terminal-based development environment.",
image="ghcr.io/opencode-ai/opencode:latest",
runtime_working_dir="/workspace",
ports=[PortConfig(container_port=3000, name="http", primary=True)],
workspace_mounts=[
MountConfig(source_pattern="{project_repo}", target="/workspace")
],
config_mounts=[
MountConfig(
source_pattern="{user_config}/opencode", target="/root/.config/opencode"
)
],
env={"TERM": "xterm-256color", "FORCE_COLOR": "1"},
health_check=HealthCheckConfig(
type="http", path="/", port=3000, start_period_seconds=15
),
resource_limits=ResourceLimits(cpus=2.0, memory_mb=4096),
traefik=TraefikConfig(
enabled=True, subdomain_prefix="opencode", port=3000
),
)
assert manifest.id == "opencode"
assert manifest.ports[0].primary is True
assert manifest.traefik is not None
assert manifest.traefik.enabled is True
def test_valid_code_server_shape() -> None:
manifest = ToolManifest(
id="code-server",
name="code-server",
description="VS Code in the browser.",
image="codercom/code-server:latest",
runtime_working_dir="/workspace",
ports=[PortConfig(container_port=8080, name="http", primary=True)],
workspace_mounts=[
MountConfig(source_pattern="{project_repo}", target="/workspace")
],
config_mounts=[
MountConfig(
source_pattern="{user_config}/code-server",
target="/home/coder/.config/code-server",
)
],
health_check=HealthCheckConfig(type="http", path="/healthz", port=8080),
resource_limits=ResourceLimits(cpus=2.0, memory_mb=4096),
traefik=TraefikConfig(enabled=True, subdomain_prefix="code", port=8080),
secrets=[
SecretRef(name="code-server-password", env_var="PASSWORD", required=False)
],
)
assert manifest.id == "code-server"
assert manifest.secrets[0].env_var == "PASSWORD"
# ---------------------------------------------------------------------------
# Invalid id values
# ---------------------------------------------------------------------------
def test_invalid_id_uppercase() -> None:
with pytest.raises(ValidationError) as exc_info:
_minimal_manifest(id="OpenCode")
assert "id" in str(exc_info.value)
def test_invalid_id_spaces() -> None:
with pytest.raises(ValidationError) as exc_info:
_minimal_manifest(id="open code")
assert "id" in str(exc_info.value)
def test_invalid_id_empty_string() -> None:
with pytest.raises(ValidationError) as exc_info:
_minimal_manifest(id="")
assert "id" in str(exc_info.value)
# ---------------------------------------------------------------------------
# Port validation
# ---------------------------------------------------------------------------
def test_invalid_container_port_zero() -> None:
with pytest.raises(ValidationError) as exc_info:
PortConfig(container_port=0)
assert "container_port" in str(exc_info.value)
def test_invalid_container_port_too_high() -> None:
with pytest.raises(ValidationError) as exc_info:
PortConfig(container_port=70000)
assert "container_port" in str(exc_info.value)
# ---------------------------------------------------------------------------
# Mount target validation
# ---------------------------------------------------------------------------
def test_mount_target_not_absolute() -> None:
with pytest.raises(ValidationError) as exc_info:
MountConfig(source_pattern="{project_repo}", target="workspace")
assert "absolute" in str(exc_info.value).lower()
# ---------------------------------------------------------------------------
# HealthCheck validation
# ---------------------------------------------------------------------------
def test_health_check_http_missing_path() -> None:
with pytest.raises(ValidationError) as exc_info:
HealthCheckConfig(type="http")
assert "path" in str(exc_info.value)
def test_health_check_command_missing_command() -> None:
with pytest.raises(ValidationError) as exc_info:
HealthCheckConfig(type="command")
assert "command" in str(exc_info.value)
def test_health_check_tcp_allows_missing_path() -> None:
hc = HealthCheckConfig(type="tcp")
assert hc.type == "tcp"
# ---------------------------------------------------------------------------
# Traefik + primary port validation
# ---------------------------------------------------------------------------
def test_missing_primary_port_when_traefik_enabled() -> None:
with pytest.raises(ValidationError) as exc_info:
ToolManifest(
id="bad-tool",
name="Bad Tool",
image="test:latest",
ports=[PortConfig(container_port=8080, primary=False)],
traefik=TraefikConfig(enabled=True),
)
assert "primary" in str(exc_info.value).lower()
def test_traefik_disabled_allows_no_primary_port() -> None:
manifest = ToolManifest(
id="no-route",
name="No Route",
image="test:latest",
ports=[PortConfig(container_port=8080, primary=False)],
traefik=TraefikConfig(enabled=False),
)
assert manifest.traefik is not None
assert manifest.traefik.enabled is False
def test_no_traefik_allows_no_primary_port() -> None:
manifest = ToolManifest(
id="no-route",
name="No Route",
image="test:latest",
ports=[PortConfig(container_port=8080, primary=False)],
)
assert manifest.traefik is None
# ---------------------------------------------------------------------------
# Resource limits validation
# ---------------------------------------------------------------------------
def test_resource_limits_cpus_too_low() -> None:
with pytest.raises(ValidationError) as exc_info:
ResourceLimits(cpus=0.001)
assert "cpus" in str(exc_info.value)
def test_resource_limits_memory_mb_too_low() -> None:
with pytest.raises(ValidationError) as exc_info:
ResourceLimits(memory_mb=8)
assert "memory_mb" in str(exc_info.value)
def test_resource_limits_memory_swap_negative_one_ok() -> None:
rl = ResourceLimits(memory_swap_mb=-1)
assert rl.memory_swap_mb == -1
# ---------------------------------------------------------------------------
# Serialization round-trip
# ---------------------------------------------------------------------------
def test_serialization_roundtrip() -> None:
original = _minimal_manifest(
id="roundtrip",
name="Roundtrip Tool",
ports=[PortConfig(container_port=3000, name="http", primary=True)],
traefik=TraefikConfig(enabled=True, subdomain_prefix="rt"),
)
dumped = original.model_dump(mode="json")
restored = ToolManifest.model_validate(dumped)
assert restored.id == original.id
assert restored.ports[0].container_port == original.ports[0].container_port
assert restored.traefik is not None
assert restored.traefik.subdomain_prefix == "rt"