Repo-side rename only (Phases 1-3 of the migration plan); the live box (~/.castle, systemd units, /data/castle, domains) is a separate cutover. - Slug `castle` -> `wildpc`: CLI command, module names (wildpc_core/cli/api), dist names, entry point `wildpc = wildpc_cli.main:main`. - Identifiers: CastleConfig/NATSClient/DirError/MDNS -> Wildpc*. - Env/constants: CASTLE_* -> WILDPC_*; ~/.castle -> ~/.wildpc, castle.yaml -> wildpc.yaml, /data/castle -> /data/wildpc. - Systemd UNIT_PREFIX castle- -> wildpc-; own programs castle-api/gateway/etc. - Display prose "Castle" -> "Wild PC" in docs, agent-guide files, README, frontend. - Package dirs and bootstrap yaml renamed via git mv; lockfiles regenerated; redundant nested uv.lock files dropped (workspace root lock is authoritative). Tests: core 273, cli 47, wildpc-api 120 all pass. Frontend type-checks + builds. Fixed a stale test fixture (secret_env_path kind arg) broken pre-rename.
88 lines
3.0 KiB
Python
88 lines
3.0 KiB
Python
"""Cross-node (remote) gateway routes + presence breaker."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
from wildpc_core.config import load_config
|
|
from wildpc_core.generators.caddyfile import compute_routes
|
|
from wildpc_core.registry import Deployment, NodeConfig, NodeRegistry
|
|
|
|
|
|
def _config_requiring_widget(root: Path) -> None:
|
|
(root / "wildpc.yaml").write_text(yaml.safe_dump({"gateway": {"port": 18000}}))
|
|
svc_dir = root / "deployments" / "services"
|
|
svc_dir.mkdir(parents=True)
|
|
(svc_dir / "consumer.yaml").write_text(
|
|
yaml.safe_dump(
|
|
{
|
|
"description": "consumes a remote widget",
|
|
"manager": "systemd",
|
|
"run": {"launcher": "command", "argv": ["consumer"]},
|
|
"requires": [{"kind": "deployment", "ref": "widget"}],
|
|
"manage": {"systemd": {}},
|
|
}
|
|
)
|
|
)
|
|
|
|
|
|
def _peer_with_widget(address: str | None) -> NodeRegistry:
|
|
widget = Deployment(
|
|
manager="systemd",
|
|
launcher="python",
|
|
run_cmd=[],
|
|
name="widget",
|
|
kind="service",
|
|
port=9099,
|
|
subdomain="widget",
|
|
managed=True,
|
|
)
|
|
return NodeRegistry(
|
|
node=NodeConfig(hostname="tower", address=address),
|
|
deployed={NodeRegistry.key("service", "widget"): widget},
|
|
)
|
|
|
|
|
|
def _local() -> NodeRegistry:
|
|
return NodeRegistry(node=NodeConfig(hostname="civil"), deployed={})
|
|
|
|
|
|
def test_remote_route_emitted_for_consumed_peer_service(tmp_path: Path) -> None:
|
|
_config_requiring_widget(tmp_path)
|
|
config = load_config(tmp_path)
|
|
routes = compute_routes(
|
|
_local(), config, {"tower": _peer_with_widget("10.0.0.5")}
|
|
)
|
|
remote = [r for r in routes if r.kind == "remote"]
|
|
assert len(remote) == 1
|
|
assert remote[0].address == "widget"
|
|
assert remote[0].target == "10.0.0.5:9099"
|
|
assert remote[0].node == "tower"
|
|
|
|
|
|
def test_address_falls_back_to_hostname(tmp_path: Path) -> None:
|
|
_config_requiring_widget(tmp_path)
|
|
config = load_config(tmp_path)
|
|
routes = compute_routes(_local(), config, {"tower": _peer_with_widget(None)})
|
|
remote = [r for r in routes if r.kind == "remote"]
|
|
assert remote[0].target == "tower:9099"
|
|
|
|
|
|
def test_breaker_no_route_when_peer_absent(tmp_path: Path) -> None:
|
|
"""Presence expiry removes the peer from remote_registries -> no route."""
|
|
_config_requiring_widget(tmp_path)
|
|
config = load_config(tmp_path)
|
|
routes = compute_routes(_local(), config, {}) # no online peers
|
|
assert not [r for r in routes if r.kind == "remote"]
|
|
|
|
|
|
def test_no_remote_route_for_unconsumed_service(tmp_path: Path) -> None:
|
|
"""A peer service nobody requires is not routed."""
|
|
(tmp_path / "wildpc.yaml").write_text(yaml.safe_dump({"gateway": {"port": 18000}}))
|
|
config = load_config(tmp_path) # no requires anywhere
|
|
routes = compute_routes(
|
|
_local(), config, {"tower": _peer_with_widget("10.0.0.5")}
|
|
)
|
|
assert not [r for r in routes if r.kind == "remote"]
|