Refactor configuration handling to use directory-per-resource layout

- Introduced `_write_castle_config` function to scatter nested config into separate YAML files for programs, services, and jobs.
- Updated `castle_root` fixture to utilize the new config writing method.
- Added tests for configuration aggregation and scattering in `test_health.py`.
- Removed legacy unit handling from `manifest.py` and refactored related code in `config.py`.
- Updated documentation to reflect new configuration directory structure.
- Removed obsolete unit tests related to unit expansion.
This commit is contained in:
2026-06-28 14:01:18 -07:00
parent 48c3915e4d
commit 5987f01749
13 changed files with 384 additions and 744 deletions

View File

@@ -17,10 +17,25 @@ from castle_core.registry import (
)
def _write_castle_config(root: Path, config: dict) -> None:
"""Scatter a nested castle config dict into the directory-per-resource layout."""
globals_data = {k: v for k, v in config.items() if k in ("gateway", "repo")}
(root / "castle.yaml").write_text(yaml.dump(globals_data, default_flow_style=False))
for section in ("programs", "services", "jobs"):
entries = config.get(section) or {}
if not entries:
continue
section_dir = root / section
section_dir.mkdir(parents=True, exist_ok=True)
for name, spec in entries.items():
(section_dir / f"{name}.yaml").write_text(
yaml.dump(spec, default_flow_style=False)
)
@pytest.fixture
def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
"""Create a temporary castle root with castle.yaml."""
castle_yaml = tmp_path / "castle.yaml"
"""Create a temporary castle root with directory-per-resource config."""
config = {
"gateway": {"port": 9000},
"programs": {
@@ -77,7 +92,7 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
},
},
}
castle_yaml.write_text(yaml.dump(config, default_flow_style=False))
_write_castle_config(tmp_path, config)
yield tmp_path
@@ -143,7 +158,14 @@ def registry_path(tmp_path: Path, castle_root: Path) -> Generator[Path, None, No
"config_editor.get_castle_root": config_editor_mod.get_castle_root,
}
for mod in [api_config, routes_mod, services_mod, nodes_mod, stream_mod, config_editor_mod]:
for mod in [
api_config,
routes_mod,
services_mod,
nodes_mod,
stream_mod,
config_editor_mod,
]:
if hasattr(mod, "get_registry"):
mod.get_registry = _get_registry
if hasattr(mod, "get_castle_root"):

View File

@@ -268,3 +268,36 @@ class TestGateway:
assert data["routes"]
for r in data["routes"]:
assert r["kind"] in ("static", "proxy", "remote")
class TestConfigEditor:
"""Virtual castle.yaml aggregation/scatter endpoints."""
def test_get_aggregates_resources(self, client: TestClient) -> None:
"""GET /config returns a unified YAML aggregating all resource files."""
import yaml
response = client.get("/config")
assert response.status_code == 200
data = yaml.safe_load(response.json()["yaml_content"])
assert "test-tool" in data["programs"]
assert "test-svc" in data["services"]
assert "test-job" in data["jobs"]
def test_put_scatters_and_prunes(self, client: TestClient, castle_root) -> None:
"""PUT /config writes resource files and prunes removed ones."""
import yaml
current = yaml.safe_load(client.get("/config").json()["yaml_content"])
current["services"].pop("test-svc")
current["programs"]["new-tool"] = {
"description": "Brand new",
"behavior": "tool",
}
resp = client.put("/config", json={"yaml_content": yaml.dump(current)})
assert resp.status_code == 200, resp.text
assert not (castle_root / "services" / "test-svc.yaml").exists()
assert (castle_root / "programs" / "new-tool.yaml").exists()
after = yaml.safe_load(client.get("/config").json()["yaml_content"])
assert "new-tool" in after["programs"]
assert "test-svc" not in (after.get("services") or {})