refactor: Separate runtime from build. Enhance configuration and registry management, migrate to registry-based component handling

This commit is contained in:
2026-02-22 23:19:16 -08:00
parent 033a76ccfd
commit d52d8829ba
37 changed files with 1271 additions and 414 deletions

View File

@@ -18,10 +18,12 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
"components": {
"test-svc": {
"description": "Test service",
"source": "test-svc",
"run": {
"runner": "python_uv_tool",
"tool": "test-svc",
"working_dir": "test-svc",
},
"defaults": {
"env": {"TEST_SVC_DATA_DIR": str(tmp_path / "data" / "test-svc")},
},
"expose": {

View File

@@ -62,7 +62,7 @@ class TestLoadConfig:
svc = config.components["test-svc"]
assert svc.run.runner == "python_uv_tool"
assert svc.run.tool == "test-svc"
assert svc.run.working_dir == "test-svc"
assert svc.source == "test-svc"
def test_tool_no_run(self, castle_root: Path) -> None:
"""Tool without run block has no run spec."""

View File

@@ -33,9 +33,7 @@ class TestRoleDerivation:
m = ComponentManifest(
id="svc",
run=RunPythonUvTool(runner="python_uv_tool", tool="svc"),
expose=ExposeSpec(
http=HttpExposeSpec(internal=HttpInternal(port=8000))
),
expose=ExposeSpec(http=HttpExposeSpec(internal=HttpInternal(port=8000))),
)
assert Role.SERVICE in m.roles
@@ -117,9 +115,7 @@ class TestRoleDerivation:
m = ComponentManifest(
id="multi",
run=RunPythonUvTool(runner="python_uv_tool", tool="multi"),
expose=ExposeSpec(
http=HttpExposeSpec(internal=HttpInternal(port=8000))
),
expose=ExposeSpec(http=HttpExposeSpec(internal=HttpInternal(port=8000))),
install=InstallSpec(path=PathInstallSpec(alias="multi")),
)
assert Role.SERVICE in m.roles
@@ -130,9 +126,7 @@ class TestRoleDerivation:
m = ComponentManifest(
id="svc",
run=RunPythonUvTool(runner="python_uv_tool", tool="svc"),
expose=ExposeSpec(
http=HttpExposeSpec(internal=HttpInternal(port=8000))
),
expose=ExposeSpec(http=HttpExposeSpec(internal=HttpInternal(port=8000))),
manage=ManageSpec(systemd=SystemdSpec()),
)
assert Role.SERVICE in m.roles
@@ -144,7 +138,9 @@ class TestConsistencyValidation:
def test_remote_with_systemd_raises(self) -> None:
"""Remote runner + systemd management is invalid."""
with pytest.raises(ValueError, match="manage.systemd cannot be enabled for runner=remote"):
with pytest.raises(
ValueError, match="manage.systemd cannot be enabled for runner=remote"
):
ComponentManifest(
id="bad",
run=RunRemote(runner="remote", base_url="http://example.com"),

View File

@@ -5,7 +5,12 @@ from __future__ import annotations
from pathlib import Path
from castle_core.config import load_config
from castle_core.generators.systemd import generate_unit, unit_name
from castle_core.generators.systemd import (
generate_unit,
generate_unit_from_deployed,
unit_name,
)
from castle_core.registry import DeployedComponent
class TestUnitName:
@@ -27,15 +32,15 @@ class TestUnitGeneration:
unit = generate_unit(config, "test-svc", manifest)
assert "Description=Castle: Test service" in unit
def test_contains_working_dir(self, castle_root: Path) -> None:
"""Unit file has correct working directory."""
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 f"WorkingDirectory={castle_root / 'test-svc'}" in unit
assert "WorkingDirectory" not in unit
def test_contains_environment(self, castle_root: Path) -> None:
"""Unit file has environment variables."""
"""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)
@@ -55,3 +60,47 @@ class TestUnitGeneration:
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_basic_service(self) -> None:
"""Generate a unit from a deployed component."""
deployed = DeployedComponent(
runner="python_uv_tool",
run_cmd=["/home/user/.local/bin/uv", "run", "my-svc"],
env={"MY_SVC_PORT": "9001", "MY_SVC_DATA_DIR": "/data/castle/my-svc"},
description="My service",
)
unit = generate_unit_from_deployed("my-svc", deployed)
assert "Description=Castle: My service" in unit
assert "ExecStart=/home/user/.local/bin/uv run my-svc" in unit
assert "Environment=MY_SVC_PORT=9001" in unit
assert "Environment=MY_SVC_DATA_DIR=/data/castle/my-svc" in unit
assert "WorkingDirectory" not in unit
assert "Restart=on-failure" in unit
def test_scheduled_job(self) -> None:
"""Scheduled component generates oneshot unit."""
deployed = DeployedComponent(
runner="command",
run_cmd=["/home/user/.local/bin/my-job"],
env={},
description="Nightly job",
schedule="0 2 * * *",
)
unit = generate_unit_from_deployed("my-job", deployed)
assert "Type=oneshot" in unit
assert "Restart=" not in unit
def test_no_repo_paths(self) -> None:
"""Generated units must not reference repo paths."""
deployed = DeployedComponent(
runner="python_uv_tool",
run_cmd=["/home/user/.local/bin/uv", "run", "my-svc"],
env={"DATA_DIR": "/data/castle/my-svc"},
description="Test",
)
unit = generate_unit_from_deployed("my-svc", deployed)
assert "/data/repos/" not in unit