feat(FN-003): add tool manifest registry with FastAPI CRUD and built-in manifests
- Add ToolManifest Pydantic models with validators for ports, mounts, health checks, and traefik config - Implement in-memory ToolRegistry with YAML loading and built-in manifest scanning - Add FastAPI CRUD routes for listing, retrieving, and creating tool manifests - Include built-in manifests for runfusion and code-server - Harden web Dockerfile with unprivileged nginx and port 8080 - Add tool manifest specification documentation and architecture updates Fusion-Task-Id: FN-003
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
"""Tests for the in-memory tool manifest registry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.tools.models import PortConfig, ToolManifest, TraefikConfig
|
||||
from app.tools.registry import ToolRegistry
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture
|
||||
def registry() -> ToolRegistry:
|
||||
return ToolRegistry()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_manifest() -> ToolManifest:
|
||||
return ToolManifest(
|
||||
id="test-tool",
|
||||
name="Test Tool",
|
||||
image="test:latest",
|
||||
ports=[PortConfig(container_port=8080, primary=True)],
|
||||
traefik=TraefikConfig(enabled=False),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Register / get round-trip
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_register_and_get(registry: ToolRegistry, sample_manifest: ToolManifest) -> None:
|
||||
registry.register(sample_manifest)
|
||||
retrieved = registry.get("test-tool")
|
||||
assert retrieved is not None
|
||||
assert retrieved.id == "test-tool"
|
||||
|
||||
|
||||
def test_get_missing_returns_none(registry: ToolRegistry) -> None:
|
||||
assert registry.get("missing") is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# List
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_list_returns_all(registry: ToolRegistry) -> None:
|
||||
m1 = ToolManifest(
|
||||
id="tool-a",
|
||||
name="Tool A",
|
||||
image="a:latest",
|
||||
ports=[PortConfig(container_port=8080, primary=True)],
|
||||
traefik=TraefikConfig(enabled=False),
|
||||
)
|
||||
m2 = ToolManifest(
|
||||
id="tool-b",
|
||||
name="Tool B",
|
||||
image="b:latest",
|
||||
ports=[PortConfig(container_port=3000, primary=True)],
|
||||
traefik=TraefikConfig(enabled=False),
|
||||
)
|
||||
registry.register(m1)
|
||||
registry.register(m2)
|
||||
assert len(registry.list()) == 2
|
||||
ids = {m.id for m in registry.list()}
|
||||
assert ids == {"tool-a", "tool-b"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Overwrite behavior
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_register_overwrites_existing(
|
||||
registry: ToolRegistry,
|
||||
sample_manifest: ToolManifest,
|
||||
) -> None:
|
||||
registry.register(sample_manifest)
|
||||
updated = ToolManifest(
|
||||
id="test-tool",
|
||||
name="Updated Tool",
|
||||
image="updated:latest",
|
||||
ports=[PortConfig(container_port=8080, primary=True)],
|
||||
traefik=TraefikConfig(enabled=False),
|
||||
)
|
||||
registry.register(updated)
|
||||
retrieved = registry.get("test-tool")
|
||||
assert retrieved is not None
|
||||
assert retrieved.name == "Updated Tool"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Remove
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_remove_returns_manifest(registry: ToolRegistry, sample_manifest: ToolManifest) -> None:
|
||||
registry.register(sample_manifest)
|
||||
removed = registry.remove("test-tool")
|
||||
assert removed is not None
|
||||
assert removed.id == "test-tool"
|
||||
assert registry.get("test-tool") is None
|
||||
|
||||
|
||||
def test_remove_missing_returns_none(registry: ToolRegistry) -> None:
|
||||
assert registry.remove("missing") is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Load file
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_load_valid_yaml_file(registry: ToolRegistry, tmp_path: Path) -> None:
|
||||
yaml_path = tmp_path / "my-tool.yml"
|
||||
yaml_path.write_text(
|
||||
"""
|
||||
id: my-tool
|
||||
name: My Tool
|
||||
image: my-tool:latest
|
||||
ports:
|
||||
- container_port: 8080
|
||||
primary: true
|
||||
traefik:
|
||||
enabled: false
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
manifest = registry.load_file(yaml_path)
|
||||
assert manifest.id == "my-tool"
|
||||
assert manifest.name == "My Tool"
|
||||
assert manifest.ports[0].container_port == 8080
|
||||
|
||||
|
||||
def test_load_invalid_yaml_raises(registry: ToolRegistry, tmp_path: Path) -> None:
|
||||
yaml_path = tmp_path / "bad-tool.yml"
|
||||
yaml_path.write_text(
|
||||
"""
|
||||
id: BAD ID
|
||||
name: Bad Tool
|
||||
image: bad:latest
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
registry.load_file(yaml_path)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Load builtin manifests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_load_builtin_manifests(registry: ToolRegistry) -> None:
|
||||
registry.load_builtin_manifests()
|
||||
# Built-in manifests from Step 4 may not exist yet in isolation,
|
||||
# but the method should not raise regardless of directory contents.
|
||||
assert isinstance(registry.list(), list)
|
||||
Reference in New Issue
Block a user