Replace the conflated `runner` axis with two orthogonal ones: `manager`
(systemd|caddy|path|none) — who supervises/realizes a deployment — and, for
systemd only, a nested `launcher` (python|command|container|compose|node) — how
the process starts. ServiceSpec and JobSpec collapse into one manager-
discriminated DeploymentSpec union (Systemd/Caddy/Path/Remote); the services/
and jobs/ config dirs collapse into one deployments/ dir. The human "kind"
(service|job|tool|static|reference) is fully derived (kind_for), never stored —
the frontend kind is renamed static. behavior is gone.
- core: DeploymentSpec union + LaunchSpec + kind_for; legacy-aware loader
normalizes old runner shapes; CastleConfig.deployments with derived
services/jobs/tools views; registry.Deployment carries manager/launcher/kind.
- cli: service/job/tool as filtered views + a deployment group; --behavior→--kind,
create --runner→--launcher; lifecycle dispatches over config.deployments.
- castle-api: /deployments primary with /services,/jobs as views; summaries
derive kind; PUT/DELETE /config/deployments/{name} (services/jobs aliased).
- app: KindBadge, frontend→static everywhere, pick-a-kind creation wizard,
per-kind config editors.
- docs: single deployments/ layout, manager/launcher, static kind throughout.
Live migration verified byte-identical: regenerated Caddyfile and every unit
ExecStart line unchanged, so nothing restarted. Suites: core 124, cli 25,
castle-api 55; dashboard build + type-check clean.
92 lines
3.2 KiB
Python
92 lines
3.2 KiB
Python
"""Tests for nodes endpoints."""
|
|
|
|
from pathlib import Path
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from castle_core.registry import Deployment, NodeConfig, NodeRegistry
|
|
|
|
from castle_api.mesh import MeshStateManager
|
|
|
|
|
|
class TestNodesList:
|
|
"""GET /nodes endpoint tests."""
|
|
|
|
def test_returns_local_node(self, client: TestClient) -> None:
|
|
"""Always returns the local node."""
|
|
response = client.get("/nodes")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data) >= 1
|
|
local = data[0]
|
|
assert local["hostname"] == "test-node"
|
|
assert local["is_local"] is True
|
|
assert local["online"] is True
|
|
|
|
def test_local_node_counts(self, client: TestClient) -> None:
|
|
"""Local node has correct deployment counts."""
|
|
response = client.get("/nodes")
|
|
data = response.json()
|
|
local = data[0]
|
|
assert local["deployed_count"] == 1 # test-svc
|
|
assert local["service_count"] == 1
|
|
|
|
def test_includes_remote_nodes(self, client: TestClient, registry_path: Path) -> None:
|
|
"""Remote nodes from mesh state are included."""
|
|
import castle_api.mesh as mesh_mod
|
|
|
|
original = mesh_mod.mesh_state
|
|
try:
|
|
mgr = MeshStateManager()
|
|
remote_reg = NodeRegistry(
|
|
node=NodeConfig(hostname="devbox", gateway_port=9000),
|
|
deployed={
|
|
"remote-svc": Deployment(
|
|
manager="systemd", launcher="python",
|
|
run_cmd=["svc"],
|
|
port=9050,
|
|
kind="service",
|
|
),
|
|
},
|
|
)
|
|
mgr.update_node("devbox", remote_reg)
|
|
mesh_mod.mesh_state = mgr
|
|
|
|
# Also patch the reference in the nodes module
|
|
import castle_api.nodes as nodes_mod
|
|
|
|
nodes_mod.mesh_state = mgr
|
|
|
|
response = client.get("/nodes")
|
|
data = response.json()
|
|
hostnames = [n["hostname"] for n in data]
|
|
assert "devbox" in hostnames
|
|
devbox = next(n for n in data if n["hostname"] == "devbox")
|
|
assert devbox["is_local"] is False
|
|
assert devbox["deployed_count"] == 1
|
|
finally:
|
|
mesh_mod.mesh_state = original
|
|
import castle_api.nodes as nodes_mod2
|
|
|
|
nodes_mod2.mesh_state = original
|
|
|
|
|
|
class TestNodeDetail:
|
|
"""GET /nodes/{hostname} endpoint tests."""
|
|
|
|
def test_local_node_detail(self, client: TestClient) -> None:
|
|
"""Returns local node detail with deployed components."""
|
|
response = client.get("/nodes/test-node")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["hostname"] == "test-node"
|
|
assert data["is_local"] is True
|
|
assert len(data["deployed"]) == 1
|
|
assert data["deployed"][0]["id"] == "test-svc"
|
|
assert data["deployed"][0]["node"] == "test-node"
|
|
|
|
def test_unknown_node_returns_404(self, client: TestClient) -> None:
|
|
"""Returns 404 for unknown hostname."""
|
|
response = client.get("/nodes/nonexistent")
|
|
assert response.status_code == 404
|