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.
105 lines
3.4 KiB
Python
105 lines
3.4 KiB
Python
"""Tests for MeshStateManager."""
|
|
|
|
import time
|
|
|
|
from castle_core.registry import Deployment, NodeConfig, NodeRegistry
|
|
|
|
from castle_api.mesh import STALE_TTL_SECONDS, MeshStateManager, RemoteNode
|
|
|
|
|
|
def _make_registry(hostname: str, deployed: dict | None = None) -> NodeRegistry:
|
|
return NodeRegistry(
|
|
node=NodeConfig(hostname=hostname, gateway_port=9000),
|
|
deployed=deployed or {},
|
|
)
|
|
|
|
|
|
class TestRemoteNode:
|
|
"""RemoteNode staleness tracking."""
|
|
|
|
def test_fresh_node_not_stale(self) -> None:
|
|
node = RemoteNode(registry=_make_registry("a"))
|
|
assert not node.is_stale
|
|
|
|
def test_old_node_is_stale(self) -> None:
|
|
node = RemoteNode(
|
|
registry=_make_registry("a"),
|
|
last_seen=time.time() - STALE_TTL_SECONDS - 1,
|
|
)
|
|
assert node.is_stale
|
|
|
|
|
|
class TestMeshStateManager:
|
|
"""MeshStateManager add/remove/stale operations."""
|
|
|
|
def test_update_and_get(self) -> None:
|
|
mgr = MeshStateManager()
|
|
reg = _make_registry("devbox")
|
|
mgr.update_node("devbox", reg)
|
|
node = mgr.get_node("devbox")
|
|
assert node is not None
|
|
assert node.registry.node.hostname == "devbox"
|
|
assert node.online is True
|
|
|
|
def test_set_offline(self) -> None:
|
|
mgr = MeshStateManager()
|
|
mgr.update_node("devbox", _make_registry("devbox"))
|
|
mgr.set_offline("devbox")
|
|
node = mgr.get_node("devbox")
|
|
assert node is not None
|
|
assert node.online is False
|
|
|
|
def test_remove_node(self) -> None:
|
|
mgr = MeshStateManager()
|
|
mgr.update_node("devbox", _make_registry("devbox"))
|
|
mgr.remove_node("devbox")
|
|
assert mgr.get_node("devbox") is None
|
|
|
|
def test_remove_nonexistent_is_safe(self) -> None:
|
|
mgr = MeshStateManager()
|
|
mgr.remove_node("nope") # should not raise
|
|
|
|
def test_all_nodes_excludes_stale(self) -> None:
|
|
mgr = MeshStateManager()
|
|
mgr.update_node("fresh", _make_registry("fresh"))
|
|
mgr._nodes["stale"] = RemoteNode(
|
|
registry=_make_registry("stale"),
|
|
last_seen=time.time() - STALE_TTL_SECONDS - 1,
|
|
)
|
|
result = mgr.all_nodes()
|
|
assert "fresh" in result
|
|
assert "stale" not in result
|
|
|
|
def test_all_nodes_includes_stale_when_requested(self) -> None:
|
|
mgr = MeshStateManager()
|
|
mgr._nodes["stale"] = RemoteNode(
|
|
registry=_make_registry("stale"),
|
|
last_seen=time.time() - STALE_TTL_SECONDS - 1,
|
|
)
|
|
result = mgr.all_nodes(include_stale=True)
|
|
assert "stale" in result
|
|
|
|
def test_prune_stale(self) -> None:
|
|
mgr = MeshStateManager()
|
|
mgr.update_node("fresh", _make_registry("fresh"))
|
|
mgr._nodes["stale"] = RemoteNode(
|
|
registry=_make_registry("stale"),
|
|
last_seen=time.time() - STALE_TTL_SECONDS - 1,
|
|
)
|
|
pruned = mgr.prune_stale()
|
|
assert pruned == ["stale"]
|
|
assert mgr.get_node("stale") is None
|
|
assert mgr.get_node("fresh") is not None
|
|
|
|
def test_update_replaces_existing(self) -> None:
|
|
mgr = MeshStateManager()
|
|
mgr.update_node("devbox", _make_registry("devbox"))
|
|
new_reg = _make_registry(
|
|
"devbox",
|
|
{"svc": Deployment(manager="systemd", launcher="python", run_cmd=["svc"])},
|
|
)
|
|
mgr.update_node("devbox", new_reg)
|
|
node = mgr.get_node("devbox")
|
|
assert node is not None
|
|
assert "svc" in node.registry.deployed
|