refactor: Enhance component info and list commands with deployed state from registry

This commit is contained in:
2026-02-23 00:07:42 -08:00
parent 5e3e01a5b6
commit 72d35f2641
7 changed files with 221 additions and 281 deletions

View File

@@ -1,60 +1,115 @@
"""Tests for Caddyfile generation."""
"""Tests for Caddyfile generation from registry."""
from __future__ import annotations
from pathlib import Path
from castle_core.config import load_config
from castle_core.generators.caddyfile import generate_caddyfile
import pytest
import castle_core.generators.caddyfile as caddyfile_mod
from castle_core.generators.caddyfile import generate_caddyfile_from_registry
from castle_core.registry import DeployedComponent, NodeConfig, NodeRegistry
class TestCaddyfileGeneration:
"""Tests for Caddyfile generation."""
@pytest.fixture(autouse=True)
def _isolate_static_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Use a temp dir for STATIC_DIR so tests don't depend on real ~/.castle."""
monkeypatch.setattr(caddyfile_mod, "STATIC_DIR", tmp_path / "static")
def test_contains_gateway_port(self, castle_root: Path) -> None:
def _make_registry(
deployed: dict[str, DeployedComponent] | None = None,
gateway_port: int = 9000,
) -> NodeRegistry:
"""Create a test registry."""
return NodeRegistry(
node=NodeConfig(hostname="test", gateway_port=gateway_port),
deployed=deployed or {},
)
class TestCaddyfileFromRegistry:
"""Tests for registry-based Caddyfile generation."""
def test_contains_gateway_port(self) -> None:
"""Caddyfile uses the configured gateway port."""
config = load_config(castle_root)
caddyfile = generate_caddyfile(config)
registry = _make_registry(gateway_port=18000)
caddyfile = generate_caddyfile_from_registry(registry)
assert ":18000 {" in caddyfile
def test_contains_service_routes(self, castle_root: Path) -> None:
"""Caddyfile has reverse proxy routes for services with proxy.caddy."""
config = load_config(castle_root)
caddyfile = generate_caddyfile(config)
def test_contains_service_routes(self) -> None:
"""Caddyfile has reverse proxy routes for deployed services."""
registry = _make_registry(
deployed={
"test-svc": DeployedComponent(
runner="python",
run_cmd=["uv", "run", "test-svc"],
port=19000,
proxy_path="/test-svc",
),
}
)
caddyfile = generate_caddyfile_from_registry(registry)
assert "handle_path /test-svc/*" in caddyfile
assert "reverse_proxy" in caddyfile
assert "19000" in caddyfile
assert "reverse_proxy localhost:19000" in caddyfile
def test_skips_tools(self, castle_root: Path) -> None:
"""Tools without proxy are not in Caddyfile."""
config = load_config(castle_root)
caddyfile = generate_caddyfile(config)
def test_skips_non_proxied(self) -> None:
"""Components without proxy_path are not in Caddyfile."""
registry = _make_registry(
deployed={
"test-tool": DeployedComponent(
runner="command",
run_cmd=["test-tool"],
),
}
)
caddyfile = generate_caddyfile_from_registry(registry)
assert "test-tool" not in caddyfile
def test_fallback_when_no_dist(self, castle_root: Path) -> None:
"""Uses fallback dashboard path when dist/ doesn't exist."""
config = load_config(castle_root)
caddyfile = generate_caddyfile(config)
# No dashboard/dist exists in tmp, so should use fallback
def test_fallback_when_no_static(self) -> None:
"""Uses fallback dashboard path when static dir doesn't exist."""
registry = _make_registry()
caddyfile = generate_caddyfile_from_registry(registry)
assert "handle / {" in caddyfile
assert "file_server" in caddyfile
def test_spa_serving_when_dist_exists(self, castle_root: Path) -> None:
"""Serves SPA with try_files when dashboard/dist exists."""
# Create a dashboard/dist with index.html
dist = castle_root / "app" / "dist"
dist.mkdir(parents=True)
(dist / "index.html").write_text("<html></html>")
config = load_config(castle_root)
caddyfile = generate_caddyfile(config)
assert "try_files {path} /index.html" in caddyfile
assert str(dist) in caddyfile
def test_proxy_routes_before_dashboard(self, castle_root: Path) -> None:
def test_proxy_routes_before_dashboard(self) -> None:
"""Service proxy routes appear before the dashboard catch-all."""
config = load_config(castle_root)
caddyfile = generate_caddyfile(config)
registry = _make_registry(
deployed={
"test-svc": DeployedComponent(
runner="python",
run_cmd=["uv", "run", "test-svc"],
port=19000,
proxy_path="/test-svc",
),
}
)
caddyfile = generate_caddyfile_from_registry(registry)
proxy_pos = caddyfile.index("handle_path")
handle_pos = caddyfile.index("handle /")
assert proxy_pos < handle_pos
def test_multiple_services(self) -> None:
"""Multiple services get separate proxy routes."""
registry = _make_registry(
deployed={
"svc-a": DeployedComponent(
runner="python",
run_cmd=["uv", "run", "svc-a"],
port=9001,
proxy_path="/svc-a",
),
"svc-b": DeployedComponent(
runner="python",
run_cmd=["uv", "run", "svc-b"],
port=9002,
proxy_path="/svc-b",
),
}
)
caddyfile = generate_caddyfile_from_registry(registry)
assert "handle_path /svc-a/*" in caddyfile
assert "reverse_proxy localhost:9001" in caddyfile
assert "handle_path /svc-b/*" in caddyfile
assert "reverse_proxy localhost:9002" in caddyfile

View File

@@ -2,11 +2,7 @@
from __future__ import annotations
from pathlib import Path
from castle_core.config import load_config
from castle_core.generators.systemd import (
generate_unit,
generate_unit_from_deployed,
unit_name,
)
@@ -22,49 +18,64 @@ class TestUnitName:
assert unit_name("my-svc") == "castle-my-svc.service"
class TestUnitGeneration:
"""Tests for systemd unit file generation."""
def test_contains_description(self, castle_root: Path) -> None:
"""Unit file has service description."""
config = load_config(castle_root)
manifest = config.components["test-svc"]
unit = generate_unit(config, "test-svc", manifest)
assert "Description=Castle: Test service" in unit
def test_no_working_directory(self, castle_root: Path) -> None:
"""Unit file has no WorkingDirectory (source/runtime separation)."""
config = load_config(castle_root)
manifest = config.components["test-svc"]
unit = generate_unit(config, "test-svc", manifest)
assert "WorkingDirectory" not in unit
def test_contains_environment(self, castle_root: Path) -> None:
"""Unit file has environment variables from defaults.env."""
config = load_config(castle_root)
manifest = config.components["test-svc"]
unit = generate_unit(config, "test-svc", manifest)
expected_data_dir = str(castle_root / "data" / "test-svc")
assert f"Environment=TEST_SVC_DATA_DIR={expected_data_dir}" in unit
def test_contains_restart_policy(self, castle_root: Path) -> None:
"""Unit file has restart configuration."""
config = load_config(castle_root)
manifest = config.components["test-svc"]
unit = generate_unit(config, "test-svc", manifest)
assert "Restart=on-failure" in unit
def test_uses_uv_run(self, castle_root: Path) -> None:
"""Unit file ExecStart uses uv run for python runner."""
config = load_config(castle_root)
manifest = config.components["test-svc"]
unit = generate_unit(config, "test-svc", manifest)
assert "run test-svc" in unit
class TestUnitFromDeployed:
"""Tests for registry-based systemd unit generation."""
def test_contains_description(self) -> None:
"""Unit file has service description."""
deployed = DeployedComponent(
runner="python",
run_cmd=["/home/user/.local/bin/uv", "run", "test-svc"],
env={"TEST_SVC_PORT": "19000"},
description="Test service",
)
unit = generate_unit_from_deployed("test-svc", deployed)
assert "Description=Castle: Test service" in unit
def test_no_working_directory(self) -> None:
"""Unit file has no WorkingDirectory (source/runtime separation)."""
deployed = DeployedComponent(
runner="python",
run_cmd=["/home/user/.local/bin/uv", "run", "test-svc"],
env={},
description="Test service",
)
unit = generate_unit_from_deployed("test-svc", deployed)
assert "WorkingDirectory" not in unit
def test_contains_environment(self) -> None:
"""Unit file has environment variables from deployed config."""
deployed = DeployedComponent(
runner="python",
run_cmd=["/home/user/.local/bin/uv", "run", "test-svc"],
env={"TEST_SVC_DATA_DIR": "/data/castle/test-svc"},
description="Test service",
)
unit = generate_unit_from_deployed("test-svc", deployed)
assert "Environment=TEST_SVC_DATA_DIR=/data/castle/test-svc" in unit
def test_contains_restart_policy(self) -> None:
"""Unit file has restart configuration."""
deployed = DeployedComponent(
runner="python",
run_cmd=["/home/user/.local/bin/uv", "run", "test-svc"],
env={},
description="Test service",
)
unit = generate_unit_from_deployed("test-svc", deployed)
assert "Restart=on-failure" in unit
def test_exec_start_from_run_cmd(self) -> None:
"""Unit file ExecStart uses resolved run_cmd."""
deployed = DeployedComponent(
runner="python",
run_cmd=["/home/user/.local/bin/uv", "run", "test-svc"],
env={},
description="Test service",
)
unit = generate_unit_from_deployed("test-svc", deployed)
assert "ExecStart=/home/user/.local/bin/uv run test-svc" in unit
def test_basic_service(self) -> None:
"""Generate a unit from a deployed component."""
deployed = DeployedComponent(