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.
142 lines
5.7 KiB
Python
142 lines
5.7 KiB
Python
"""Tests for `wildpc apply` convergence — the diff classification (plan mode).
|
|
|
|
Plan mode computes the activate/restart/deactivate/unchanged buckets without
|
|
writing or touching the runtime, so it's the deterministic way to test the diff.
|
|
`is_active` is patched to control the "before" state; unit bytes come from the
|
|
(empty) temp home, so a live systemd service with no prior unit reads as changed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import wildpc_core.deploy as deploy_mod
|
|
from wildpc_core.config import load_config
|
|
from wildpc_core.deploy import (
|
|
_desired_registry,
|
|
_gateway_would_change,
|
|
_render_unit_preview,
|
|
apply,
|
|
generate_caddyfile_from_registry,
|
|
)
|
|
from wildpc_core.registry import Deployment
|
|
|
|
|
|
def _add_static(wildpc_root: Path, name: str = "test-static") -> None:
|
|
"""Write a caddy (static) program + deployment into an existing wildpc root."""
|
|
(wildpc_root / "programs" / f"{name}.yaml").write_text(
|
|
f"description: Static {name}\nsource: {wildpc_root / name}\n"
|
|
)
|
|
statics = wildpc_root / "deployments" / "statics"
|
|
statics.mkdir(parents=True, exist_ok=True)
|
|
(statics / f"{name}.yaml").write_text(
|
|
f"program: {name}\nmanager: caddy\nroot: public\nreach: internal\n"
|
|
)
|
|
(wildpc_root / name / "public").mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def _plan(wildpc_root: Path, active: dict[str, bool]):
|
|
"""Run apply(plan=True) with is_active stubbed to `active` (default False)."""
|
|
with patch(
|
|
"wildpc_core.lifecycle.is_active",
|
|
side_effect=lambda n, k, c: active.get(n, False),
|
|
):
|
|
return apply(root=wildpc_root, plan=True)
|
|
|
|
|
|
class TestApplyPlan:
|
|
def test_fresh_converge_activates_enabled(self, wildpc_root: Path) -> None:
|
|
"""Nothing running → every enabled deployment is 'activate'; no writes."""
|
|
result = _plan(wildpc_root, active={})
|
|
|
|
assert result.planned is True
|
|
assert set(result.activated) == {"test-svc", "test-tool", "test-job"}
|
|
assert result.deactivated == []
|
|
assert result.restarted == []
|
|
|
|
def test_disabled_active_deployment_deactivates(self, wildpc_root: Path) -> None:
|
|
"""A deployment with enabled:false that's currently up → 'deactivate'."""
|
|
# Turn the tool off in config.
|
|
tool = wildpc_root / "deployments" / "tools" / "test-tool.yaml"
|
|
tool.write_text(tool.read_text() + "enabled: false\n")
|
|
|
|
result = _plan(wildpc_root, active={"test-tool": True})
|
|
|
|
assert "test-tool" in result.deactivated
|
|
assert "test-tool" not in result.activated
|
|
|
|
def test_disabled_inactive_is_unchanged(self, wildpc_root: Path) -> None:
|
|
"""enabled:false and already down → nothing to do."""
|
|
tool = wildpc_root / "deployments" / "tools" / "test-tool.yaml"
|
|
tool.write_text(tool.read_text() + "enabled: false\n")
|
|
|
|
result = _plan(wildpc_root, active={})
|
|
|
|
assert "test-tool" in result.unchanged
|
|
assert "test-tool" not in result.deactivated
|
|
|
|
def test_active_service_with_changed_unit_restarts(self, wildpc_root: Path) -> None:
|
|
"""An up systemd service whose rendered unit differs from disk → 'restart'.
|
|
|
|
The temp home has no prior unit file (before-bytes = None), so any live
|
|
systemd deployment classifies as changed → restart, not a silent no-op.
|
|
"""
|
|
result = _plan(wildpc_root, active={"test-svc": True})
|
|
|
|
assert "test-svc" in result.restarted
|
|
assert "test-svc" not in result.activated
|
|
assert result.changed is True
|
|
|
|
|
|
class TestGatewayChange:
|
|
"""A caddy route change touches no systemd unit, so the activate/restart/
|
|
deactivate reconcile can't see it. `gateway_changed` catches it by diffing the
|
|
would-be Caddyfile/tunnel config against disk — otherwise a new/changed static
|
|
route reports a false 'already converged'.
|
|
|
|
SPECS_DIR is the real ~/.wildpc path (unpatched by the fixtures), so these
|
|
redirect it to a temp dir to stay hermetic and never touch the live Caddyfile.
|
|
"""
|
|
|
|
def test_new_route_reports_gateway_changed(
|
|
self, wildpc_root: Path, tmp_path: Path, monkeypatch
|
|
) -> None:
|
|
"""A static whose route isn't on disk yet → gateway_changed, even when the
|
|
assets already exist so the deployment itself classifies 'unchanged'."""
|
|
monkeypatch.setattr(deploy_mod, "SPECS_DIR", tmp_path / "specs")
|
|
_add_static(wildpc_root)
|
|
|
|
# Static is 'active' (built) → _classify buckets it 'unchanged'; the route is
|
|
# still absent from the (missing) Caddyfile, so the gateway did change.
|
|
result = _plan(wildpc_root, active={"test-static": True})
|
|
|
|
assert "test-static" in result.unchanged
|
|
assert result.gateway_changed is True
|
|
assert result.changed is True
|
|
|
|
def test_converged_caddyfile_is_not_changed(
|
|
self, wildpc_root: Path, tmp_path: Path, monkeypatch
|
|
) -> None:
|
|
"""When the on-disk Caddyfile already matches the desired one, no change."""
|
|
specs = tmp_path / "specs"
|
|
specs.mkdir(parents=True, exist_ok=True)
|
|
monkeypatch.setattr(deploy_mod, "SPECS_DIR", specs)
|
|
_add_static(wildpc_root)
|
|
config = load_config(wildpc_root)
|
|
|
|
(specs / "Caddyfile").write_text(
|
|
generate_caddyfile_from_registry(_desired_registry(config, None))
|
|
)
|
|
|
|
assert _gateway_would_change(config, None) is False
|
|
|
|
|
|
def test_render_unit_preview_none_for_non_systemd() -> None:
|
|
"""Non-systemd managers have no unit file — preview is None (never 'restart').
|
|
|
|
A path deployment is unmanaged, so the renderer returns before touching config.
|
|
"""
|
|
tool = Deployment(manager="path", run_cmd=[], kind="tool")
|
|
assert _render_unit_preview(None, "x", tool, "tool") is None # type: ignore[arg-type]
|