Rename Castle -> Wild PC across the repo

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.
This commit is contained in:
2026-07-18 22:55:08 -07:00
parent 25d7a522cc
commit 05b28cb584
190 changed files with 2428 additions and 3337 deletions

View File

@@ -0,0 +1,102 @@
"""A tool, a service, and a job may share a name — kind-scoped endpoints must
address (and mutate) exactly one twin.
These guard the collision case the per-kind identity refactor enables: a
`backup` service + job + tool coexisting. The risk is a save/delete against one
kind bleeding into a same-named twin of another kind. We assert against the
on-disk config (load_config) so we're testing the persisted invariant, not a
response echo.
"""
from __future__ import annotations
from pathlib import Path
from fastapi.testclient import TestClient
from wildpc_core.config import load_config
# Minimal, valid specs for each kind — all named "backup", all referencing the
# same program. Only the service is HTTP-exposed (none claims a subdomain here),
# so the trio passes subdomain-uniqueness validation.
_SVC = {
"program": "backup",
"manager": "systemd",
"run": {"launcher": "python", "program": "backup"},
"manage": {"systemd": {}},
}
_JOB = {
"program": "backup",
"manager": "systemd",
"run": {"launcher": "command", "argv": ["backup"]},
"schedule": "0 3 * * *",
}
_TOOL = {"program": "backup", "manager": "path"}
def _put(client: TestClient, section: str, name: str, cfg: dict) -> None:
r = client.put(f"/config/{section}/{name}", json={"config": cfg})
assert r.status_code == 200, r.text
class TestKindScopedTwins:
def test_same_name_across_kinds_coexist(
self, client: TestClient, wildpc_root: Path
) -> None:
"""Creating a service, job, and tool all named `backup` yields three
distinct deployments — one per kind, none overwriting another."""
_put(client, "services", "backup", _SVC)
_put(client, "jobs", "backup", _JOB)
_put(client, "tools", "backup", _TOOL)
cfg = load_config(wildpc_root)
assert "backup" in cfg.services
assert "backup" in cfg.jobs
assert "backup" in cfg.tools
def test_detail_endpoints_resolve_the_right_twin(
self, client: TestClient, wildpc_root: Path
) -> None:
"""`/services/backup` and `/jobs/backup` each return their own kind, not
whichever twin happens to sort first."""
_put(client, "services", "backup", _SVC)
_put(client, "jobs", "backup", _JOB)
svc = client.get("/services/backup").json()
job = client.get("/jobs/backup").json()
assert svc["kind"] == "service"
# Each endpoint returns its own twin: the job carries the schedule, the
# service does not — so neither resolved to the other.
assert job["manifest"].get("schedule") == "0 3 * * *"
assert "schedule" not in svc["manifest"]
def test_kind_scoped_save_does_not_touch_the_twin(
self, client: TestClient, wildpc_root: Path
) -> None:
"""A partial patch to the *service* backup must leave the *job* backup
(and its schedule) untouched — the wrong-twin bleed this refactor closes."""
_put(client, "services", "backup", _SVC)
_put(client, "jobs", "backup", _JOB)
_put(client, "services", "backup", {"reach": "off"})
cfg = load_config(wildpc_root)
assert cfg.jobs["backup"].schedule == "0 3 * * *" # job untouched
assert "backup" in cfg.services # service still there
def test_kind_scoped_delete_removes_only_that_twin(
self, client: TestClient, wildpc_root: Path
) -> None:
"""Deleting `/config/tools/backup` drops only the tool; the service and
job twins survive."""
_put(client, "services", "backup", _SVC)
_put(client, "jobs", "backup", _JOB)
_put(client, "tools", "backup", _TOOL)
r = client.delete("/config/tools/backup")
assert r.status_code == 200, r.text
cfg = load_config(wildpc_root)
assert "backup" not in cfg.tools # tool gone
assert "backup" in cfg.services # twins survive
assert "backup" in cfg.jobs