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

@@ -1,4 +1,4 @@
"""Shared fixtures for castle core tests."""
"""Shared fixtures for wildpc core tests."""
from __future__ import annotations
@@ -6,9 +6,9 @@ import os as _os
from collections.abc import Generator
from pathlib import Path
# Tests must not read the host's real secret backend (castle.yaml may point
# Tests must not read the host's real secret backend (wildpc.yaml may point
# at OpenBao); force the file backend unless CI explicitly overrides.
_os.environ.setdefault("CASTLE_SECRET_BACKEND", "file")
_os.environ.setdefault("WILDPC_SECRET_BACKEND", "file")
import pytest
import yaml
@@ -64,16 +64,16 @@ def _store_for(spec: dict) -> str:
}[spec["manager"]]
def write_castle_config(root: Path, config: dict) -> None:
"""Scatter a nested castle config dict into the on-disk layout.
def write_wildpc_config(root: Path, config: dict) -> None:
"""Scatter a nested wildpc config dict into the on-disk layout.
`config` uses the terse nested shape (gateway/repo at top level, plus
programs/services/jobs mappings); this writes castle.yaml with globals, one file
programs/services/jobs mappings); this writes wildpc.yaml with globals, one file
per program under programs/, and each deployment under deployments/<kind>/ after
modernizing its legacy fields (see `_modernize_deployment`).
"""
globals_data = {k: v for k, v in config.items() if k in ("gateway", "repo")}
(root / "castle.yaml").write_text(yaml.dump(globals_data, default_flow_style=False))
(root / "wildpc.yaml").write_text(yaml.dump(globals_data, default_flow_style=False))
programs = config.get("programs") or {}
if programs:
@@ -94,8 +94,8 @@ def write_castle_config(root: Path, config: dict) -> None:
@pytest.fixture
def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
"""Create a temporary castle root with directory-per-resource config."""
def wildpc_root(tmp_path: Path) -> Generator[Path, None, None]:
"""Create a temporary wildpc root with directory-per-resource config."""
config = {
"gateway": {"port": 18000},
"programs": {
@@ -141,7 +141,7 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
},
},
}
write_castle_config(tmp_path, config)
write_wildpc_config(tmp_path, config)
# Create project directories
svc_dir = tmp_path / "test-svc"
@@ -155,9 +155,9 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
@pytest.fixture
def castle_home(tmp_path: Path) -> Generator[Path, None, None]:
"""Create a temporary ~/.castle directory."""
home = tmp_path / ".castle"
def wildpc_home(tmp_path: Path) -> Generator[Path, None, None]:
"""Create a temporary ~/.wildpc directory."""
home = tmp_path / ".wildpc"
home.mkdir()
(home / "generated").mkdir()
(home / "secrets").mkdir()

View File

@@ -1,4 +1,4 @@
"""Tests for `castle apply` convergence — the diff classification (plan mode).
"""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.
@@ -11,78 +11,78 @@ from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import castle_core.deploy as deploy_mod
from castle_core.config import load_config
from castle_core.deploy import (
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 castle_core.registry import Deployment
from wildpc_core.registry import Deployment
def _add_static(castle_root: Path, name: str = "test-static") -> None:
"""Write a caddy (static) program + deployment into an existing castle root."""
(castle_root / "programs" / f"{name}.yaml").write_text(
f"description: Static {name}\nsource: {castle_root / name}\n"
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 = castle_root / "deployments" / "statics"
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"
)
(castle_root / name / "public").mkdir(parents=True, exist_ok=True)
(wildpc_root / name / "public").mkdir(parents=True, exist_ok=True)
def _plan(castle_root: Path, active: dict[str, bool]):
def _plan(wildpc_root: Path, active: dict[str, bool]):
"""Run apply(plan=True) with is_active stubbed to `active` (default False)."""
with patch(
"castle_core.lifecycle.is_active",
"wildpc_core.lifecycle.is_active",
side_effect=lambda n, k, c: active.get(n, False),
):
return apply(root=castle_root, plan=True)
return apply(root=wildpc_root, plan=True)
class TestApplyPlan:
def test_fresh_converge_activates_enabled(self, castle_root: Path) -> None:
def test_fresh_converge_activates_enabled(self, wildpc_root: Path) -> None:
"""Nothing running → every enabled deployment is 'activate'; no writes."""
result = _plan(castle_root, active={})
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, castle_root: Path) -> None:
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 = castle_root / "deployments" / "tools" / "test-tool.yaml"
tool = wildpc_root / "deployments" / "tools" / "test-tool.yaml"
tool.write_text(tool.read_text() + "enabled: false\n")
result = _plan(castle_root, active={"test-tool": True})
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, castle_root: Path) -> None:
def test_disabled_inactive_is_unchanged(self, wildpc_root: Path) -> None:
"""enabled:false and already down → nothing to do."""
tool = castle_root / "deployments" / "tools" / "test-tool.yaml"
tool = wildpc_root / "deployments" / "tools" / "test-tool.yaml"
tool.write_text(tool.read_text() + "enabled: false\n")
result = _plan(castle_root, active={})
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, castle_root: Path) -> None:
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(castle_root, active={"test-svc": True})
result = _plan(wildpc_root, active={"test-svc": True})
assert "test-svc" in result.restarted
assert "test-svc" not in result.activated
@@ -95,35 +95,35 @@ class TestGatewayChange:
would-be Caddyfile/tunnel config against disk — otherwise a new/changed static
route reports a false 'already converged'.
SPECS_DIR is the real ~/.castle path (unpatched by the fixtures), so these
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, castle_root: Path, tmp_path: Path, monkeypatch
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(castle_root)
_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(castle_root, active={"test-static": True})
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, castle_root: Path, tmp_path: Path, monkeypatch
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(castle_root)
config = load_config(castle_root)
_add_static(wildpc_root)
config = load_config(wildpc_root)
(specs / "Caddyfile").write_text(
generate_caddyfile_from_registry(_desired_registry(config, None))

View File

@@ -1,10 +1,10 @@
"""Tests for the consumption audit (core/src/castle_core/audit.py)."""
"""Tests for the consumption audit (core/src/wildpc_core/audit.py)."""
from __future__ import annotations
import castle_core.config as C
from castle_core import audit
from castle_core.manifest import SystemdDeployment
import wildpc_core.config as C
from wildpc_core import audit
from wildpc_core.manifest import SystemdDeployment
def _svc(
@@ -33,8 +33,8 @@ def _svc(
return SystemdDeployment.model_validate(spec)
def _cfg(deployments: dict) -> C.CastleConfig:
return C.CastleConfig(
def _cfg(deployments: dict) -> C.WildpcConfig:
return C.WildpcConfig(
root=None,
gateway=C.GatewayConfig(port=9000),
repo=None,
@@ -43,7 +43,7 @@ def _cfg(deployments: dict) -> C.CastleConfig:
)
def _pairs(cfg: C.CastleConfig) -> set[tuple[str, str]]:
def _pairs(cfg: C.WildpcConfig) -> set[tuple[str, str]]:
return {(s.consumer, s.provider) for s in audit.suggest_consumption(cfg)}
@@ -55,7 +55,7 @@ def test_split_host_port_pair_is_suggested() -> None:
"api": _svc(
"api",
http=9020,
env={"CASTLE_API_MQTT_HOST": "localhost", "CASTLE_API_MQTT_PORT": "1883"},
env={"WILDPC_API_MQTT_HOST": "localhost", "WILDPC_API_MQTT_PORT": "1883"},
),
}
)

View File

@@ -4,12 +4,12 @@ from __future__ import annotations
import pytest
from castle_core.config import CastleConfig, GatewayConfig
from castle_core.generators.caddyfile import (
from wildpc_core.config import WildpcConfig, GatewayConfig
from wildpc_core.generators.caddyfile import (
compute_routes,
generate_caddyfile_from_registry,
)
from castle_core.manifest import (
from wildpc_core.manifest import (
CaddyDeployment,
DeploymentSpec,
ExposeSpec,
@@ -20,14 +20,14 @@ from castle_core.manifest import (
RunPython,
SystemdDeployment,
)
from castle_core.registry import Deployment, NodeConfig, NodeRegistry
from wildpc_core.registry import Deployment, NodeConfig, NodeRegistry
@pytest.fixture(autouse=True)
def _isolate_config(monkeypatch: pytest.MonkeyPatch) -> None:
"""Isolate the generator from the real ~/.castle config so static-frontend
"""Isolate the generator from the real ~/.wildpc config so static-frontend
routes don't leak into these registry-focused tests."""
import castle_core.config as config_mod
import wildpc_core.config as config_mod
def _no_config(*args: object, **kwargs: object) -> object:
raise FileNotFoundError("isolated in tests")
@@ -101,7 +101,7 @@ class TestAcmeMode:
def test_port_9000_redirects_to_dashboard(self) -> None:
cf = generate_caddyfile_from_registry(_acme({"api": _dep(9020, expose=True, name="api")}))
assert ":9000 {" in cf
assert "redir https://castle.example.com{uri}" in cf
assert "redir https://wildpc.example.com{uri}" in cf
def test_no_path_routes(self) -> None:
cf = generate_caddyfile_from_registry(_acme({"api": _dep(9020, expose=True, name="api")}))
@@ -114,7 +114,7 @@ class TestAcmeMode:
assert "pg.example.com" not in cf
def test_staging_toggle(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("CASTLE_ACME_STAGING", "1")
monkeypatch.setenv("WILDPC_ACME_STAGING", "1")
cf = generate_caddyfile_from_registry(_acme({"api": _dep(9020, expose=True, name="api")}))
assert "acme_ca https://acme-staging-v02.api.letsencrypt.org/directory" in cf
@@ -122,20 +122,20 @@ class TestAcmeMode:
self, monkeypatch: pytest.MonkeyPatch
) -> None:
# A static frontend is a `runner: static` service serving <source>/<root>.
import castle_core.config as config_mod
import wildpc_core.config as config_mod
cfg = _config(
deployments={
"castle": CaddyDeployment(
manager="caddy", program="castle", root="dist"
"wildpc": CaddyDeployment(
manager="caddy", program="wildpc", root="dist"
)
},
programs={"castle": ProgramSpec(source="/data/repos/castle/app")},
programs={"wildpc": ProgramSpec(source="/data/repos/wildpc/app")},
)
monkeypatch.setattr(config_mod, "load_config", lambda *a, **k: cfg)
cf = generate_caddyfile_from_registry(_acme({}))
assert "@host_castle host castle.example.com" in cf
assert "root * /data/repos/castle/app/dist" in cf
assert "@host_wildpc host wildpc.example.com" in cf
assert "root * /data/repos/wildpc/app/dist" in cf
assert "try_files {path} /index.html" in cf
assert "file_server" in cf
@@ -187,12 +187,12 @@ class TestPublicExposure:
class TestOffMode:
"""No domain → HTTP-only control plane on :<port>: dashboard at / + /api → castle-api.
"""No domain → HTTP-only control plane on :<port>: dashboard at / + /api → wildpc-api.
Other services are port-only (not routed)."""
def test_control_plane(self) -> None:
cf = generate_caddyfile_from_registry(
_make_registry(deployed={"castle-api": _dep(9020, expose=True, name="castle-api")})
_make_registry(deployed={"wildpc-api": _dep(9020, expose=True, name="wildpc-api")})
)
assert "auto_https off" in cf
assert "handle_path /api/* {" in cf
@@ -217,8 +217,8 @@ def _service(port: int, *, expose: bool) -> SystemdDeployment:
def _config(
deployments: dict[str, DeploymentSpec], programs: dict[str, ProgramSpec] | None = None
) -> CastleConfig:
return CastleConfig(
) -> WildpcConfig:
return WildpcConfig(
root=None, # type: ignore[arg-type]
gateway=GatewayConfig(port=9000),
repo=None,
@@ -228,7 +228,7 @@ def _config(
class TestConfigSourceOfTruth:
"""compute_routes derives exposure/port from castle.yaml (the checkbox), so a
"""compute_routes derives exposure/port from wildpc.yaml (the checkbox), so a
regenerated Caddyfile tracks the spec, not a stale registry."""
def test_exposed_service_becomes_a_route(self) -> None:

View File

@@ -5,13 +5,13 @@ from __future__ import annotations
from pathlib import Path
import yaml
from castle_core.config import load_config
from castle_core.generators.caddyfile import compute_routes
from castle_core.registry import Deployment, NodeConfig, NodeRegistry
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 / "castle.yaml").write_text(yaml.safe_dump({"gateway": {"port": 18000}}))
(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(
@@ -79,7 +79,7 @@ def test_breaker_no_route_when_peer_absent(tmp_path: Path) -> None:
def test_no_remote_route_for_unconsumed_service(tmp_path: Path) -> None:
"""A peer service nobody requires is not routed."""
(tmp_path / "castle.yaml").write_text(yaml.safe_dump({"gateway": {"port": 18000}}))
(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")}

View File

@@ -1,4 +1,4 @@
"""Tests for castle configuration loading."""
"""Tests for wildpc configuration loading."""
from __future__ import annotations
@@ -6,138 +6,138 @@ from pathlib import Path
import pytest
import yaml
from castle_core.config import (
CastleConfig,
from wildpc_core.config import (
WildpcConfig,
load_config,
resolve_env_split,
resolve_env_vars,
save_config,
secret_refs,
)
from castle_core.manifest import ProgramSpec, SystemdDeployment
from wildpc_core.manifest import ProgramSpec, SystemdDeployment
class TestLoadConfig:
"""Tests for loading castle.yaml."""
"""Tests for loading wildpc.yaml."""
def test_load_basic(self, castle_root: Path) -> None:
"""Load a castle.yaml with three sections."""
config = load_config(castle_root)
assert isinstance(config, CastleConfig)
def test_load_basic(self, wildpc_root: Path) -> None:
"""Load a wildpc.yaml with three sections."""
config = load_config(wildpc_root)
assert isinstance(config, WildpcConfig)
assert config.gateway.port == 18000
assert "test-tool" in config.programs
assert "test-svc" in config.services
assert "test-job" in config.jobs
def test_load_produces_typed_specs(self, castle_root: Path) -> None:
def test_load_produces_typed_specs(self, wildpc_root: Path) -> None:
"""Each section produces the correct spec type."""
config = load_config(castle_root)
config = load_config(wildpc_root)
assert isinstance(config.programs["test-tool"], ProgramSpec)
# Both a service and a job are systemd deployments; the kind (service/job)
# is derived from whether a schedule is present.
assert isinstance(config.services["test-svc"], SystemdDeployment)
assert isinstance(config.jobs["test-job"], SystemdDeployment)
def test_service_expose(self, castle_root: Path) -> None:
def test_service_expose(self, wildpc_root: Path) -> None:
"""Service has correct expose spec."""
config = load_config(castle_root)
config = load_config(wildpc_root)
svc = config.services["test-svc"]
assert svc.expose.http.internal.port == 19000
assert svc.expose.http.health_path == "/health"
def test_service_proxy(self, castle_root: Path) -> None:
def test_service_proxy(self, wildpc_root: Path) -> None:
"""Service has correct proxy spec."""
config = load_config(castle_root)
config = load_config(wildpc_root)
svc = config.services["test-svc"]
assert svc.proxy is True # exposed at <name>.<gateway.domain>
def test_service_run_spec(self, castle_root: Path) -> None:
def test_service_run_spec(self, wildpc_root: Path) -> None:
"""Service has correct launch spec (legacy runner normalized to launcher)."""
config = load_config(castle_root)
config = load_config(wildpc_root)
svc = config.services["test-svc"]
assert svc.run.launcher == "python"
assert svc.run.program == "test-svc"
def test_service_component_ref(self, castle_root: Path) -> None:
def test_service_component_ref(self, wildpc_root: Path) -> None:
"""Service references a component."""
config = load_config(castle_root)
config = load_config(wildpc_root)
svc = config.services["test-svc"]
assert svc.program == "test-svc-comp"
def test_job_schedule(self, castle_root: Path) -> None:
def test_job_schedule(self, wildpc_root: Path) -> None:
"""Job has correct schedule."""
config = load_config(castle_root)
config = load_config(wildpc_root)
job = config.jobs["test-job"]
assert job.schedule == "0 2 * * *"
def test_tools_property(self, castle_root: Path) -> None:
def test_tools_property(self, wildpc_root: Path) -> None:
"""Tools property filters to components with install.path or tool."""
config = load_config(castle_root)
config = load_config(wildpc_root)
assert "test-tool" in config.tools
def test_missing_config_raises(self, tmp_path: Path) -> None:
"""Missing castle.yaml raises FileNotFoundError."""
"""Missing wildpc.yaml raises FileNotFoundError."""
with pytest.raises(FileNotFoundError):
load_config(tmp_path)
class TestSaveConfig:
"""Tests for saving castle.yaml."""
"""Tests for saving wildpc.yaml."""
def test_round_trip(self, castle_root: Path) -> None:
def test_round_trip(self, wildpc_root: Path) -> None:
"""Load and save should produce equivalent config."""
config = load_config(castle_root)
config = load_config(wildpc_root)
save_config(config)
config2 = load_config(castle_root)
config2 = load_config(wildpc_root)
assert config2.gateway.port == config.gateway.port
assert set(config2.programs.keys()) == set(config.programs.keys())
assert set(config2.services.keys()) == set(config.services.keys())
assert set(config2.jobs.keys()) == set(config.jobs.keys())
def test_save_adds_component(self, castle_root: Path) -> None:
def test_save_adds_component(self, wildpc_root: Path) -> None:
"""Adding a component and saving persists it."""
config = load_config(castle_root)
config = load_config(wildpc_root)
config.programs["new-lib"] = ProgramSpec(
id="new-lib", description="A new library"
)
save_config(config)
config2 = load_config(castle_root)
config2 = load_config(wildpc_root)
assert "new-lib" in config2.programs
assert config2.programs["new-lib"].description == "A new library"
def test_preserves_manage_systemd(self, castle_root: Path) -> None:
def test_preserves_manage_systemd(self, wildpc_root: Path) -> None:
"""Roundtrip preserves manage.systemd even with all defaults."""
config = load_config(castle_root)
config = load_config(wildpc_root)
save_config(config)
config2 = load_config(castle_root)
config2 = load_config(wildpc_root)
svc = config2.services["test-svc"]
assert svc.manage is not None
assert svc.manage.systemd is not None
def test_writes_directory_layout(self, castle_root: Path) -> None:
def test_writes_directory_layout(self, wildpc_root: Path) -> None:
"""Save writes one file per resource under programs/ and one deployments/ dir."""
config = load_config(castle_root)
config = load_config(wildpc_root)
save_config(config)
assert (castle_root / "programs" / "test-tool.yaml").exists()
assert (wildpc_root / "programs" / "test-tool.yaml").exists()
# Deployments live under per-kind subdirs (deployments/<store>/<name>.yaml).
assert (castle_root / "deployments" / "services" / "test-svc.yaml").exists()
assert (castle_root / "deployments" / "jobs" / "test-job.yaml").exists()
assert (castle_root / "deployments" / "tools" / "test-tool.yaml").exists()
assert (wildpc_root / "deployments" / "services" / "test-svc.yaml").exists()
assert (wildpc_root / "deployments" / "jobs" / "test-job.yaml").exists()
assert (wildpc_root / "deployments" / "tools" / "test-tool.yaml").exists()
# Global file holds gateway only, no resource sections
global_data = yaml.safe_load((castle_root / "castle.yaml").read_text())
global_data = yaml.safe_load((wildpc_root / "wildpc.yaml").read_text())
assert global_data["gateway"]["port"] == 18000
assert "programs" not in global_data
assert "deployments" not in global_data
def test_delete_prunes_file(self, castle_root: Path) -> None:
def test_delete_prunes_file(self, wildpc_root: Path) -> None:
"""Removing a deployment and saving deletes its on-disk file."""
config = load_config(castle_root)
config = load_config(wildpc_root)
del config.services["test-svc"]
save_config(config)
assert not (castle_root / "deployments" / "services" / "test-svc.yaml").exists()
config2 = load_config(castle_root)
assert not (wildpc_root / "deployments" / "services" / "test-svc.yaml").exists()
config2 = load_config(wildpc_root)
assert "test-svc" not in config2.services
assert "test-tool" in config2.programs
@@ -164,7 +164,7 @@ class TestResolveEnvVars:
secrets_dir = tmp_path / "secrets"
secrets_dir.mkdir()
(secrets_dir / "API_KEY").write_text("my-secret-key\n")
monkeypatch.setattr("castle_core.config.SECRETS_DIR", secrets_dir)
monkeypatch.setattr("wildpc_core.config.SECRETS_DIR", secrets_dir)
env = {"API_KEY": "${secret:API_KEY}"}
resolved = resolve_env_vars(env)
@@ -176,7 +176,7 @@ class TestResolveEnvVars:
"""Missing secret returns placeholder."""
secrets_dir = tmp_path / "secrets"
secrets_dir.mkdir()
monkeypatch.setattr("castle_core.config.SECRETS_DIR", secrets_dir)
monkeypatch.setattr("wildpc_core.config.SECRETS_DIR", secrets_dir)
env = {"API_KEY": "${secret:NONEXISTENT}"}
resolved = resolve_env_vars(env)
@@ -207,7 +207,7 @@ class TestResolveEnvSplit:
secrets_dir.mkdir()
for name, val in vals.items():
(secrets_dir / name).write_text(val + "\n")
monkeypatch.setattr("castle_core.config.SECRETS_DIR", secrets_dir)
monkeypatch.setattr("wildpc_core.config.SECRETS_DIR", secrets_dir)
def test_plain_only(self) -> None:
plain, secret = resolve_env_split({"PORT": "9001", "URL": "http://x"})
@@ -273,8 +273,8 @@ class TestConfigRoundTrip:
the next write — these lock the full round-trip for the reach/TCP-TLS fields."""
def test_gateway_and_tcp_tls_survive_save_load(self, tmp_path: Path) -> None:
from castle_core.config import GatewayConfig
from castle_core.manifest import (
from wildpc_core.config import GatewayConfig
from wildpc_core.manifest import (
ExposeSpec,
Reach,
RunContainer,
@@ -299,7 +299,7 @@ class TestConfigRoundTrip:
tcp=TcpExposeSpec(port=5432, tls=TlsSpec(material=TlsMaterial.PAIR))
),
)
config = CastleConfig(
config = WildpcConfig(
root=tmp_path,
gateway=GatewayConfig(
port=9000, tls="acme", domain="civil.payne.io", cert_hook=True
@@ -327,7 +327,7 @@ class TestConfigRoundTrip:
def test_parse_gateway_preserves_all_fields(self) -> None:
"""The shared gateway parser must honor every field — `save_yaml` used to
read only `port`, wiping tls/domain/tunnel/cert_hook on a whole-file save."""
from castle_core.config import parse_gateway
from wildpc_core.config import parse_gateway
g = parse_gateway(
{
@@ -349,27 +349,27 @@ class TestConfigRoundTrip:
class TestConfigurableRoots:
"""data_dir / repos_dir: env > castle.yaml > default. The single source of truth
"""data_dir / repos_dir: env > wildpc.yaml > default. The single source of truth
that keeps the CLI and the api service from resolving different data dirs."""
@pytest.fixture(autouse=True)
def _no_root_env(self, monkeypatch: pytest.MonkeyPatch) -> None:
# The test host may export CASTLE_DATA_DIR (that's the bug we're fixing);
# The test host may export WILDPC_DATA_DIR (that's the bug we're fixing);
# clear it so yaml/default precedence is exercised deterministically.
monkeypatch.delenv("CASTLE_DATA_DIR", raising=False)
monkeypatch.delenv("CASTLE_REPOS_DIR", raising=False)
monkeypatch.delenv("WILDPC_DATA_DIR", raising=False)
monkeypatch.delenv("WILDPC_REPOS_DIR", raising=False)
def test_resolve_precedence_env_over_yaml(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
from castle_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
from wildpc_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
monkeypatch.setenv("X_ROOT_ENV", "/from/env")
p = _resolve_root_path("X_ROOT_ENV", "/from/yaml", tmp_path, _DEFAULT_DATA_DIR)
assert p == Path("/from/env")
def test_resolve_yaml_over_default(self, tmp_path: Path) -> None:
from castle_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
from wildpc_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
p = _resolve_root_path(
"UNSET_ROOT_ENV", "/from/yaml", tmp_path, _DEFAULT_DATA_DIR
@@ -377,7 +377,7 @@ class TestConfigurableRoots:
assert p == Path("/from/yaml")
def test_resolve_default_when_neither(self, tmp_path: Path) -> None:
from castle_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
from wildpc_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
p = _resolve_root_path("UNSET_ROOT_ENV", None, tmp_path, _DEFAULT_DATA_DIR)
assert p == _DEFAULT_DATA_DIR # returned as-is so save_config can compare equal
@@ -385,16 +385,16 @@ class TestConfigurableRoots:
def test_resolve_expanduser(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
from castle_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
from wildpc_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
monkeypatch.setenv("X_ROOT_ENV", "~/box")
p = _resolve_root_path("X_ROOT_ENV", None, tmp_path, _DEFAULT_DATA_DIR)
assert p == (Path.home() / "box").resolve()
def test_resolve_relative_anchored_to_anchor_not_cwd(self, tmp_path: Path) -> None:
"""A relative root is anchored to the dir holding castle.yaml — never cwd, or
"""A relative root is anchored to the dir holding wildpc.yaml — never cwd, or
the CLI (shell cwd) and api (unit cwd) would diverge again."""
from castle_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
from wildpc_core.config import _DEFAULT_DATA_DIR, _resolve_root_path
p = _resolve_root_path(
"UNSET_ROOT_ENV", "sub/data", tmp_path, _DEFAULT_DATA_DIR
@@ -402,23 +402,23 @@ class TestConfigurableRoots:
assert p == (tmp_path / "sub" / "data").resolve()
def test_load_config_reads_data_dir_from_yaml(self, tmp_path: Path) -> None:
(tmp_path / "castle.yaml").write_text(
(tmp_path / "wildpc.yaml").write_text(
yaml.dump({"gateway": {"port": 9000}, "data_dir": "/srv/box/data"})
)
config = load_config(tmp_path)
assert config.data_dir == Path("/srv/box/data")
def test_load_config_data_dir_defaults(self, tmp_path: Path) -> None:
from castle_core.config import _DEFAULT_DATA_DIR
from wildpc_core.config import _DEFAULT_DATA_DIR
(tmp_path / "castle.yaml").write_text(yaml.dump({"gateway": {"port": 9000}}))
(tmp_path / "wildpc.yaml").write_text(yaml.dump({"gateway": {"port": 9000}}))
config = load_config(tmp_path)
assert config.data_dir == _DEFAULT_DATA_DIR
def test_save_round_trips_nondefault_roots(self, tmp_path: Path) -> None:
from castle_core.config import GatewayConfig
from wildpc_core.config import GatewayConfig
cfg = CastleConfig(
cfg = WildpcConfig(
root=tmp_path,
gateway=GatewayConfig(port=9000),
repo=None,
@@ -427,7 +427,7 @@ class TestConfigurableRoots:
repos_dir=Path("/srv/box/repos"),
)
save_config(cfg)
text = (tmp_path / "castle.yaml").read_text()
text = (tmp_path / "wildpc.yaml").read_text()
assert "data_dir: /srv/box/data" in text
assert "repos_dir: /srv/box/repos" in text
reloaded = load_config(tmp_path)
@@ -435,13 +435,13 @@ class TestConfigurableRoots:
assert reloaded.repos_dir == Path("/srv/box/repos")
def test_save_omits_default_roots(self, tmp_path: Path) -> None:
from castle_core.config import (
from wildpc_core.config import (
_DEFAULT_DATA_DIR,
_DEFAULT_REPOS_DIR,
GatewayConfig,
)
cfg = CastleConfig(
cfg = WildpcConfig(
root=tmp_path,
gateway=GatewayConfig(port=9000),
repo=None,
@@ -450,20 +450,20 @@ class TestConfigurableRoots:
repos_dir=_DEFAULT_REPOS_DIR,
)
save_config(cfg)
text = (tmp_path / "castle.yaml").read_text()
text = (tmp_path / "wildpc.yaml").read_text()
assert "data_dir" not in text
assert "repos_dir" not in text
def test_ensure_dirs_raises_actionable_error(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""An uncreatable data dir yields a CastleDirError with a fix, not a bare OSError."""
import castle_core.config as C
from castle_core.config import GatewayConfig
"""An uncreatable data dir yields a WildpcDirError with a fix, not a bare OSError."""
import wildpc_core.config as C
from wildpc_core.config import GatewayConfig
# Redirect the in-$HOME dirs to tmp so the test never touches the real ~/.castle.
# Redirect the in-$HOME dirs to tmp so the test never touches the real ~/.wildpc.
for name in (
"CASTLE_HOME",
"WILDPC_HOME",
"CODE_DIR",
"SPECS_DIR",
"CONTENT_DIR",
@@ -474,13 +474,13 @@ class TestConfigurableRoots:
# deterministically, even if the suite runs as root.
blocker = tmp_path / "afile"
blocker.write_text("x")
cfg = CastleConfig(
cfg = WildpcConfig(
root=tmp_path,
gateway=GatewayConfig(port=9000),
repo=None,
programs={},
data_dir=blocker / "sub",
)
with pytest.raises(C.CastleDirError) as ei:
with pytest.raises(C.WildpcDirError) as ei:
C.ensure_dirs(cfg)
assert "data_dir" in str(ei.value)

View File

@@ -5,9 +5,9 @@ from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
from castle_core import deploy as deploy_mod
from castle_core.deploy import _desired_unit_files, _prune_orphans
from castle_core.registry import Deployment, NodeConfig, NodeRegistry
from wildpc_core import deploy as deploy_mod
from wildpc_core.deploy import _desired_unit_files, _prune_orphans
from wildpc_core.registry import Deployment, NodeConfig, NodeRegistry
def _svc(managed: bool = True, schedule: str | None = None) -> Deployment:
@@ -23,7 +23,7 @@ def _svc(managed: bool = True, schedule: str | None = None) -> Deployment:
def _registry(**deployed: Deployment) -> NodeRegistry:
reg = NodeRegistry(node=NodeConfig(castle_root="/x", gateway_port=9000))
reg = NodeRegistry(node=NodeConfig(wildpc_root="/x", gateway_port=9000))
for name, d in deployed.items():
d.name = name
reg.put(d)
@@ -38,11 +38,11 @@ def _touch(d: Path, *names: str) -> None:
class TestDesiredUnitFiles:
def test_managed_service_yields_service_file(self) -> None:
reg = _registry(foo=_svc())
assert _desired_unit_files(reg) == {"castle-foo.service"}
assert _desired_unit_files(reg) == {"wildpc-foo.service"}
def test_scheduled_job_yields_service_and_timer(self) -> None:
reg = _registry(job=_svc(schedule="0 2 * * *"))
assert _desired_unit_files(reg) == {"castle-job-job.service", "castle-job-job.timer"}
assert _desired_unit_files(reg) == {"wildpc-job-job.service", "wildpc-job-job.timer"}
def test_unmanaged_excluded(self) -> None:
reg = _registry(foo=_svc(managed=False))
@@ -54,7 +54,7 @@ class TestPruneOrphans:
units = tmp_path / "systemd"
units.mkdir()
# keep-me is in the registry; gone is not; gone.timer is also orphaned
_touch(units, "castle-keep.service", "castle-gone.service", "castle-gone.timer")
_touch(units, "wildpc-keep.service", "wildpc-gone.service", "wildpc-gone.timer")
reg = _registry(keep=_svc())
msgs: list[str] = []
with (
@@ -62,17 +62,17 @@ class TestPruneOrphans:
patch.object(deploy_mod.subprocess, "run") as mock_run,
):
_prune_orphans(reg, msgs)
assert (units / "castle-keep.service").exists()
assert not (units / "castle-gone.service").exists()
assert not (units / "castle-gone.timer").exists()
assert (units / "wildpc-keep.service").exists()
assert not (units / "wildpc-gone.service").exists()
assert not (units / "wildpc-gone.timer").exists()
# stop + disable were invoked for each removed unit
assert mock_run.call_count >= 2
assert any("castle-gone.service" in m for m in msgs)
assert any("wildpc-gone.service" in m for m in msgs)
def test_unscheduled_job_drops_stale_timer_keeps_service(self, tmp_path: Path) -> None:
units = tmp_path / "systemd"
units.mkdir()
_touch(units, "castle-job.service", "castle-job.timer")
_touch(units, "wildpc-job.service", "wildpc-job.timer")
# job is still managed but no longer scheduled → its timer is now an orphan
reg = _registry(job=_svc(schedule=None))
with (
@@ -80,17 +80,17 @@ class TestPruneOrphans:
patch.object(deploy_mod.subprocess, "run"),
):
_prune_orphans(reg, [])
assert (units / "castle-job.service").exists()
assert not (units / "castle-job.timer").exists()
assert (units / "wildpc-job.service").exists()
assert not (units / "wildpc-job.timer").exists()
def test_demoted_to_unmanaged_is_pruned(self, tmp_path: Path) -> None:
units = tmp_path / "systemd"
units.mkdir()
_touch(units, "castle-foo.service")
_touch(units, "wildpc-foo.service")
reg = _registry(foo=_svc(managed=False)) # still in registry but unmanaged
with (
patch.object(deploy_mod, "SYSTEMD_USER_DIR", units),
patch.object(deploy_mod.subprocess, "run"),
):
_prune_orphans(reg, [])
assert not (units / "castle-foo.service").exists()
assert not (units / "wildpc-foo.service").exists()

View File

@@ -5,14 +5,14 @@ from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
from castle_core.deploy import _build_run_cmd, _build_stop_cmd
from castle_core.manifest import RunCompose, RunContainer, RunNode, RunPython
from wildpc_core.deploy import _build_run_cmd, _build_stop_cmd
from wildpc_core.manifest import RunCompose, RunContainer, RunNode, RunPython
def test_python_runner_uses_uv_run_from_source(tmp_path: Path) -> None:
"""A python service with a real source dir launches via `uv run --project`."""
run = RunPython(launcher="python", program="my-svc")
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/uv"):
with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/uv"):
cmd = _build_run_cmd("my-svc", run, {}, [], source_dir=tmp_path)
assert cmd == [
"/usr/bin/uv",
@@ -26,7 +26,7 @@ def test_python_runner_uses_uv_run_from_source(tmp_path: Path) -> None:
def test_python_runner_appends_args(tmp_path: Path) -> None:
run = RunPython(launcher="python", program="my-svc", args=["--flag", "x"])
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/uv"):
with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/uv"):
cmd = _build_run_cmd("my-svc", run, {}, [], source_dir=tmp_path)
assert cmd[-2:] == ["--flag", "x"]
assert cmd[:5] == ["/usr/bin/uv", "run", "--project", str(tmp_path), "--no-dev"]
@@ -36,7 +36,7 @@ def test_python_runner_falls_back_to_path_without_source() -> None:
"""No resolvable source → PATH lookup of the script (no uv run)."""
run = RunPython(launcher="python", program="my-svc")
with patch(
"castle_core.deploy.shutil.which", return_value="/home/u/.local/bin/my-svc"
"wildpc_core.deploy.shutil.which", return_value="/home/u/.local/bin/my-svc"
):
cmd = _build_run_cmd("my-svc", run, {}, [], source_dir=None)
assert cmd == ["/home/u/.local/bin/my-svc"]
@@ -46,7 +46,7 @@ def test_python_runner_warns_when_unresolvable() -> None:
"""No source and not on PATH → a warning, bare program name as last resort."""
run = RunPython(launcher="python", program="my-svc")
messages: list[str] = []
with patch("castle_core.deploy.shutil.which", return_value=None):
with patch("wildpc_core.deploy.shutil.which", return_value=None):
cmd = _build_run_cmd("my-svc", run, {}, messages, source_dir=None)
assert cmd == ["my-svc"]
assert any("my-svc" in m for m in messages)
@@ -55,7 +55,7 @@ def test_python_runner_warns_when_unresolvable() -> None:
def test_node_runner_bakes_source_dir(tmp_path: Path) -> None:
"""A node service runs the script in its source dir via `--dir` (no unit cwd)."""
run = RunNode(launcher="node", script="gateway:watch:raw", package_manager="pnpm")
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/pnpm"):
with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/pnpm"):
cmd = _build_run_cmd("oc", run, {}, [], source_dir=tmp_path)
assert cmd == ["/usr/bin/pnpm", "--dir", str(tmp_path), "run", "gateway:watch:raw"]
@@ -64,7 +64,7 @@ def test_node_runner_appends_args(tmp_path: Path) -> None:
run = RunNode(
launcher="node", script="start", package_manager="pnpm", args=["--port", "18789"]
)
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/pnpm"):
with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/pnpm"):
cmd = _build_run_cmd("oc", run, {}, [], source_dir=tmp_path)
assert cmd == [
"/usr/bin/pnpm",
@@ -80,7 +80,7 @@ def test_node_runner_appends_args(tmp_path: Path) -> None:
def test_node_runner_without_source_omits_dir() -> None:
"""No resolvable source → bare `pnpm run` (package manager still PATH-resolved)."""
run = RunNode(launcher="node", script="start", package_manager="pnpm")
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/pnpm"):
with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/pnpm"):
cmd = _build_run_cmd("oc", run, {}, [], source_dir=None)
assert cmd == ["/usr/bin/pnpm", "run", "start"]
@@ -88,8 +88,8 @@ def test_node_runner_without_source_omits_dir() -> None:
def test_container_secrets_use_env_file_not_argv() -> None:
"""Secrets go through --env-file; plain vars stay as -e; no secret in argv."""
run = RunContainer(launcher="container", image="img:latest", env={"PLAIN": "1"})
env_file = Path("/home/u/.castle/secrets/env/castle-svc.service.env")
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"):
env_file = Path("/home/u/.wildpc/secrets/env/wildpc-svc.service.env")
with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/docker"):
cmd = _build_run_cmd("svc", run, {"PORT": "9001"}, [], secret_env_file=env_file)
joined = " ".join(cmd)
assert "--env-file" in cmd
@@ -102,7 +102,7 @@ def test_container_secrets_use_env_file_not_argv() -> None:
def test_container_without_secrets_has_no_env_file() -> None:
run = RunContainer(launcher="container", image="img:latest")
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"):
with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/docker"):
cmd = _build_run_cmd("svc", run, {}, [], secret_env_file=None)
assert "--env-file" not in cmd
@@ -120,42 +120,42 @@ def test_container_placeholders_expand_in_run_fields() -> None:
ph = {
"name": "svc",
"port": "5432",
"data_dir": "/data/castle/svc",
"tls_dir": "/data/castle/svc/tls",
"data_dir": "/data/wildpc/svc",
"tls_dir": "/data/wildpc/svc/tls",
}
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"):
with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/docker"):
cmd = _build_run_cmd("svc", run, {}, [], placeholders=ph)
joined = " ".join(cmd)
assert "DATA=/data/castle/svc/x" in joined # env expanded
assert "/data/castle/svc/tls:/tls:ro" in joined # volume expanded
assert "DATA=/data/wildpc/svc/x" in joined # env expanded
assert "/data/wildpc/svc/tls:/tls:ro" in joined # volume expanded
assert "svc:5432" in cmd # arg expanded
def test_container_double_dollar_escapes_placeholder() -> None:
"""$${key} passes a literal ${key} through to the container's own shell/env
instead of castle expanding it (docker-compose-style escape)."""
instead of wildpc expanding it (docker-compose-style escape)."""
run = RunContainer(
launcher="container",
image="img:latest",
args=["sh", "-c", "exec myd --advertise $${name}:${port}"],
)
ph = {"name": "svc", "port": "5432"}
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"):
with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/docker"):
cmd = _build_run_cmd("svc", run, {}, [], placeholders=ph)
# castle expands ${port} but leaves $${name} as a literal ${name} for the shell
# wildpc expands ${port} but leaves $${name} as a literal ${name} for the shell
assert "exec myd --advertise ${name}:5432" in cmd
def test_compose_runner_up_with_resolved_file(tmp_path: Path) -> None:
"""A compose service runs `docker compose -p <project> -f <abs-file> up`."""
run = RunCompose(launcher="compose", file="docker-compose.yml")
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"):
with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/docker"):
cmd = _build_run_cmd("supabase", run, {}, [], source_dir=tmp_path)
assert cmd == [
"/usr/bin/docker",
"compose",
"-p",
"castle-supabase",
"wildpc-supabase",
"-f",
str(tmp_path / "docker-compose.yml"),
"up",
@@ -165,8 +165,8 @@ def test_compose_runner_up_with_resolved_file(tmp_path: Path) -> None:
def test_compose_runner_secrets_never_hit_argv(tmp_path: Path) -> None:
"""Compose reads env from the process (systemd), not --env-file/-e — argv is clean."""
run = RunCompose(launcher="compose", file="docker-compose.yml")
env_file = Path("/home/u/.castle/secrets/env/castle-supabase.service.env")
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"):
env_file = Path("/home/u/.wildpc/secrets/env/wildpc-supabase.service.env")
with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/docker"):
cmd = _build_run_cmd(
"supabase", run, {"PORT": "8000"}, [], source_dir=tmp_path,
secret_env_file=env_file,
@@ -179,7 +179,7 @@ def test_compose_runner_secrets_never_hit_argv(tmp_path: Path) -> None:
def test_compose_project_name_override(tmp_path: Path) -> None:
run = RunCompose(launcher="compose", file="stack.yml", project_name="myproj")
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"):
with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/docker"):
cmd = _build_run_cmd("s", run, {}, [], source_dir=tmp_path)
assert cmd[:6] == [
"/usr/bin/docker", "compose", "-p", "myproj", "-f", str(tmp_path / "stack.yml"),
@@ -189,13 +189,13 @@ def test_compose_project_name_override(tmp_path: Path) -> None:
def test_compose_stop_cmd_is_down(tmp_path: Path) -> None:
"""The teardown command mirrors up but ends in `down` (same project + file)."""
run = RunCompose(launcher="compose", file="docker-compose.yml")
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"):
with patch("wildpc_core.deploy.shutil.which", return_value="/usr/bin/docker"):
stop = _build_stop_cmd("supabase", run, tmp_path)
assert stop == [
"/usr/bin/docker",
"compose",
"-p",
"castle-supabase",
"wildpc-supabase",
"-f",
str(tmp_path / "docker-compose.yml"),
"down",

View File

@@ -7,8 +7,8 @@ from pathlib import Path
import pytest
import castle_core.deploy as deploy
from castle_core.registry import (
import wildpc_core.deploy as deploy
from wildpc_core.registry import (
Deployment,
NodeConfig,
NodeRegistry,
@@ -23,7 +23,10 @@ def secret_env_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
d = tmp_path / "secrets" / "env"
monkeypatch.setattr(deploy, "SECRET_ENV_DIR", d)
monkeypatch.setattr(
deploy, "secret_env_path", lambda name: d / f"castle-{name}.service.env"
deploy,
"secret_env_path",
lambda name, kind="service": d
/ f"wildpc-{name}{'-job' if kind == 'job' else ''}.service.env",
)
return d

View File

@@ -1,6 +1,6 @@
"""Tests for the apply-time unresolved-secret gate.
`castle apply` must refuse to converge a deployment whose ``${secret:NAME}`` env
`wildpc apply` must refuse to converge a deployment whose ``${secret:NAME}`` env
references the active backend can't resolve — otherwise the value silently
degrades to a ``<MISSING_SECRET:NAME>`` placeholder and the service starts with a
bogus credential (the immich-DB-password-to-the-wrong-backend failure).
@@ -13,7 +13,7 @@ from types import SimpleNamespace
import pytest
from castle_core.deploy import _unresolved_secrets
from wildpc_core.deploy import _unresolved_secrets
def _spec(env: dict[str, str], enabled: bool = True) -> SimpleNamespace:
@@ -26,8 +26,8 @@ def file_backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
secrets = tmp_path / "secrets"
secrets.mkdir()
(secrets / "PRESENT").write_text("value\n")
monkeypatch.setenv("CASTLE_SECRET_BACKEND", "file")
monkeypatch.setattr("castle_core.config.SECRETS_DIR", secrets)
monkeypatch.setenv("WILDPC_SECRET_BACKEND", "file")
monkeypatch.setattr("wildpc_core.config.SECRETS_DIR", secrets)
return secrets

View File

@@ -2,8 +2,8 @@
from __future__ import annotations
import castle_core.generators.dns as dns
from castle_core.generators.dns import _zone_for, reconcile_public_dns
import wildpc_core.generators.dns as dns
from wildpc_core.generators.dns import _zone_for, reconcile_public_dns
TID = "tid-abc"
TARGET = f"{TID}.cfargotunnel.com"
@@ -18,7 +18,7 @@ class _FakeCloudflare:
"""A minimal fake of the Cloudflare API used by reconcile_public_dns.
``records`` maps zone_id -> {name: (record_id, content)}. Records POST/DELETE
calls so tests can assert exactly what castle created/removed.
calls so tests can assert exactly what wildpc created/removed.
"""
def __init__(self, records: dict[str, dict[str, tuple[str, str]]]):
@@ -75,11 +75,11 @@ def test_creates_apex_and_subdomain_in_correct_zones(monkeypatch) -> None:
def test_deletes_managed_cname_no_longer_desired(monkeypatch) -> None:
# z_payne has a stale castle-managed CNAME + a hand-managed one pointing elsewhere.
# z_payne has a stale wildpc-managed CNAME + a hand-managed one pointing elsewhere.
fake = _FakeCloudflare(records={
"z_payne": {
"payne.io": ("r1", TARGET), # castle-managed, still desired
"old.payne.io": ("r2", TARGET), # castle-managed, now stale → delete
"payne.io": ("r1", TARGET), # wildpc-managed, still desired
"old.payne.io": ("r2", TARGET), # wildpc-managed, now stale → delete
"keep.payne.io": ("r3", "other.example.com"), # NOT ours → never touched
},
"z_ex": {},

View File

@@ -5,8 +5,8 @@ from __future__ import annotations
from pathlib import Path
import yaml
from castle_core.config import load_config
from castle_core.registry import (
from wildpc_core.config import load_config
from wildpc_core.registry import (
NodeConfig,
NodeRegistry,
load_registry,
@@ -18,7 +18,7 @@ def _write_min_config(root: Path, role: str | None) -> None:
data: dict = {"gateway": {"port": 18000}}
if role is not None:
data["role"] = role
(root / "castle.yaml").write_text(yaml.safe_dump(data))
(root / "wildpc.yaml").write_text(yaml.safe_dump(data))
def test_role_defaults_to_follower(tmp_path: Path) -> None:
@@ -32,12 +32,12 @@ def test_role_loaded_from_yaml(tmp_path: Path) -> None:
def test_save_config_round_trips_role_and_secrets(tmp_path: Path) -> None:
"""save_config rewrites castle.yaml from scratch — it must re-emit `role` and
"""save_config rewrites wildpc.yaml from scratch — it must re-emit `role` and
preserve the `secrets:` block, or a save reverts the node to a file-backend
follower (the regression this guards)."""
from castle_core.config import load_config, save_config
from wildpc_core.config import load_config, save_config
(tmp_path / "castle.yaml").write_text(
(tmp_path / "wildpc.yaml").write_text(
yaml.safe_dump(
{
"gateway": {"port": 18000},
@@ -47,57 +47,57 @@ def test_save_config_round_trips_role_and_secrets(tmp_path: Path) -> None:
)
)
save_config(load_config(tmp_path))
reloaded = yaml.safe_load((tmp_path / "castle.yaml").read_text())
reloaded = yaml.safe_load((tmp_path / "wildpc.yaml").read_text())
assert reloaded.get("role") == "authority"
assert reloaded.get("secrets", {}).get("backend") == "openbao"
def test_save_config_preserves_arbitrary_unmanaged_global(tmp_path: Path) -> None:
"""Any top-level key save_config doesn't model must survive a rewrite."""
from castle_core.config import load_config, save_config
from wildpc_core.config import load_config, save_config
(tmp_path / "castle.yaml").write_text(
(tmp_path / "wildpc.yaml").write_text(
yaml.safe_dump(
{"gateway": {"port": 18000}, "role": "authority", "future_thing": {"x": 1}}
)
)
save_config(load_config(tmp_path))
reloaded = yaml.safe_load((tmp_path / "castle.yaml").read_text())
reloaded = yaml.safe_load((tmp_path / "wildpc.yaml").read_text())
assert reloaded.get("future_thing") == {"x": 1}
assert reloaded.get("role") == "authority"
def test_write_deployment_file_leaves_globals_untouched(castle_root: Path) -> None:
"""A scoped deployment write must not rewrite castle.yaml globals (the PATCH
def test_write_deployment_file_leaves_globals_untouched(wildpc_root: Path) -> None:
"""A scoped deployment write must not rewrite wildpc.yaml globals (the PATCH
guarantee that stops a deployment edit from dropping role/secrets)."""
from castle_core.config import load_config, write_deployment_file
from wildpc_core.config import load_config, write_deployment_file
cy = castle_root / "castle.yaml"
cy = wildpc_root / "wildpc.yaml"
data = yaml.safe_load(cy.read_text())
data["role"] = "authority"
data["secrets"] = {"backend": "openbao"}
cy.write_text(yaml.safe_dump(data))
before = cy.read_text()
config = load_config(castle_root)
config = load_config(wildpc_root)
kind, name, _dep = next(iter(config.all_deployments()))
write_deployment_file(config, kind, name)
assert cy.read_text() == before # globals byte-identical — nothing touched them
def test_write_program_file_leaves_globals_untouched(castle_root: Path) -> None:
"""Scoped program write must not rewrite castle.yaml globals either."""
from castle_core.config import load_config, write_program_file
def test_write_program_file_leaves_globals_untouched(wildpc_root: Path) -> None:
"""Scoped program write must not rewrite wildpc.yaml globals either."""
from wildpc_core.config import load_config, write_program_file
cy = castle_root / "castle.yaml"
cy = wildpc_root / "wildpc.yaml"
data = yaml.safe_load(cy.read_text())
data["role"] = "authority"
data["secrets"] = {"backend": "openbao"}
cy.write_text(yaml.safe_dump(data))
before = cy.read_text()
config = load_config(castle_root)
config = load_config(wildpc_root)
name = next(iter(config.programs))
write_program_file(config, name)

View File

@@ -1,4 +1,4 @@
"""Tests for git working-copy status/sync (core/src/castle_core/git.py)."""
"""Tests for git working-copy status/sync (core/src/wildpc_core/git.py)."""
from __future__ import annotations
@@ -7,7 +7,7 @@ from pathlib import Path
import pytest
from castle_core import git as G
from wildpc_core import git as G
# Identity so commits succeed without touching the user's global git config.
_ENV = {

View File

@@ -9,9 +9,9 @@ from pathlib import Path
import pytest
import yaml
from castle_core.config import load_config, save_config
from castle_core.deploy import _build_deployed, _desired_unit_files
from castle_core.registry import NodeConfig, NodeRegistry
from wildpc_core.config import load_config, save_config
from wildpc_core.deploy import _build_deployed, _desired_unit_files
from wildpc_core.registry import NodeConfig, NodeRegistry
def _write(root: Path, store: str, name: str, spec: dict) -> None:
@@ -21,7 +21,7 @@ def _write(root: Path, store: str, name: str, spec: dict) -> None:
def _trio(root: Path) -> None:
(root / "castle.yaml").write_text(yaml.dump({"gateway": {"port": 9000}}))
(root / "wildpc.yaml").write_text(yaml.dump({"gateway": {"port": 9000}}))
_write(root, "services", "backup", {
"manager": "systemd", "run": {"launcher": "python", "program": "backup"},
})
@@ -67,14 +67,14 @@ def test_service_and_job_get_distinct_units(tmp_path: Path) -> None:
dep.name = name
registry.put(dep)
units = _desired_unit_files(registry)
# service keeps castle-<name>.service; job carries the -job marker; tool has none.
assert "castle-backup.service" in units
assert "castle-backup-job.service" in units
assert "castle-backup-job.timer" in units
# service keeps wildpc-<name>.service; job carries the -job marker; tool has none.
assert "wildpc-backup.service" in units
assert "wildpc-backup-job.service" in units
assert "wildpc-backup-job.timer" in units
def test_service_and_static_cannot_share_a_subdomain(tmp_path: Path) -> None:
(tmp_path / "castle.yaml").write_text(yaml.dump({"gateway": {"port": 9000}}))
(tmp_path / "wildpc.yaml").write_text(yaml.dump({"gateway": {"port": 9000}}))
_write(tmp_path, "services", "app", {
"manager": "systemd", "run": {"launcher": "python", "program": "app"},
"reach": "internal", "expose": {"http": {"internal": {"port": 9001}}},

View File

@@ -5,63 +5,63 @@ from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
from castle_core import lifecycle
from castle_core.config import load_config
from wildpc_core import lifecycle
from wildpc_core.config import load_config
class TestIsActive:
def test_service_uses_systemctl(self, castle_root: Path) -> None:
config = load_config(castle_root)
def test_service_uses_systemctl(self, wildpc_root: Path) -> None:
config = load_config(wildpc_root)
with patch.object(lifecycle, "_systemctl_active", return_value=True) as mock:
assert lifecycle.is_active("test-svc", "service", config) is True
mock.assert_called_once_with("castle-test-svc.service")
mock.assert_called_once_with("wildpc-test-svc.service")
def test_job_uses_timer(self, castle_root: Path) -> None:
config = load_config(castle_root)
def test_job_uses_timer(self, wildpc_root: Path) -> None:
config = load_config(wildpc_root)
with patch.object(lifecycle, "_systemctl_active", return_value=True) as mock:
assert lifecycle.is_active("test-job", "job", config) is True
mock.assert_called_once_with("castle-test-job-job.timer")
mock.assert_called_once_with("wildpc-test-job-job.timer")
def test_tool_checks_path(self, castle_root: Path) -> None:
config = load_config(castle_root)
def test_tool_checks_path(self, wildpc_root: Path) -> None:
config = load_config(wildpc_root)
# give the tool a source so the tool branch is reachable
config.programs["test-tool"].source = "/tmp/test-tool"
with patch.object(lifecycle, "_on_path", return_value=True) as mock:
assert lifecycle.is_active("test-tool", "tool", config) is True
mock.assert_called_once_with("test-tool")
def test_unknown_is_inactive(self, castle_root: Path) -> None:
config = load_config(castle_root)
def test_unknown_is_inactive(self, wildpc_root: Path) -> None:
config = load_config(wildpc_root)
assert lifecycle.is_active("does-not-exist", "service", config) is False
def test_path_deployment_checks_path(self, castle_root: Path) -> None:
def test_path_deployment_checks_path(self, wildpc_root: Path) -> None:
# A `manager: path` deployment (a tool) is active when on PATH.
from castle_core.manifest import PathDeployment, ProgramSpec
from wildpc_core.manifest import PathDeployment, ProgramSpec
config = load_config(castle_root)
config = load_config(wildpc_root)
config.programs["mytool"] = ProgramSpec(id="mytool", source="/tmp/mytool")
config.tools["mytool"] = PathDeployment(manager="path", program="mytool")
with patch.object(lifecycle, "_on_path", return_value=True) as mock:
assert lifecycle.is_active("mytool", "tool", config) is True
mock.assert_called_once_with("mytool")
def test_remote_deployment_is_active(self, castle_root: Path) -> None:
def test_remote_deployment_is_active(self, wildpc_root: Path) -> None:
# A remote deployment has no local process; the manager is `none` → available.
from castle_core.manifest import RemoteDeployment
from wildpc_core.manifest import RemoteDeployment
config = load_config(castle_root)
config = load_config(wildpc_root)
config.references["ext"] = RemoteDeployment(
manager="none", program="ext", base_url="http://x"
)
assert lifecycle.is_active("ext", "reference", config) is True
def test_static_deployment_active_when_dist_built(
self, castle_root: Path, tmp_path: Path
self, wildpc_root: Path, tmp_path: Path
) -> None:
# A static (caddy) deployment is active once its served dir exists.
from castle_core.manifest import CaddyDeployment, ProgramSpec
from wildpc_core.manifest import CaddyDeployment, ProgramSpec
config = load_config(castle_root)
config = load_config(wildpc_root)
repo = tmp_path / "fe"
config.programs["fe"] = ProgramSpec(id="fe", source=str(repo))
config.statics["fe"] = CaddyDeployment(

View File

@@ -1,9 +1,9 @@
"""Tests for castle manifest models."""
"""Tests for wildpc manifest models."""
from __future__ import annotations
import pytest
from castle_core.manifest import (
from wildpc_core.manifest import (
Reach,
BuildSpec,
CaddyDeployment,

View File

@@ -1,18 +1,18 @@
"""Tests for the relationship model (core/src/castle_core/relations.py)."""
"""Tests for the relationship model (core/src/wildpc_core/relations.py)."""
from __future__ import annotations
import pytest
import castle_core.config as C
from castle_core import relations as R
from castle_core.manifest import (
import wildpc_core.config as C
from wildpc_core import relations as R
from wildpc_core.manifest import (
CaddyDeployment,
ProgramSpec,
Requirement,
SystemdDeployment,
)
from castle_core.stacks import tools_for
from wildpc_core.stacks import tools_for
def _dep(program: str, requires: list | None = None) -> SystemdDeployment:
@@ -32,8 +32,8 @@ def _static(program: str) -> CaddyDeployment:
)
def _cfg(programs: dict, deployments: dict) -> C.CastleConfig:
return C.CastleConfig(
def _cfg(programs: dict, deployments: dict) -> C.WildpcConfig:
return C.WildpcConfig(
root=None,
gateway=C.GatewayConfig(port=9000),
repo=None,
@@ -141,7 +141,7 @@ def test_stack_tool_missing_from_service_path_is_unmet(
) -> None:
"""The drift case: uv resolves in the caller's shell PATH but NOT on the
service's curated runtime PATH → unmet *for the service*, even though a bare
`which` (what `castle tool list` uses) would report it present."""
`which` (what `wildpc tool list` uses) would report it present."""
prog = ProgramSpec(id="svc", stack="python-fastapi")
cfg = _cfg({"svc": prog}, {"svc": _dep("svc")})
shell_only = "/home/me/.shell-tools" # a dir the runtime PATH never includes

View File

@@ -4,8 +4,8 @@ from __future__ import annotations
from pathlib import Path
from castle_core.manifest import ProgramSpec
from castle_core.stacks import (
from wildpc_core.manifest import ProgramSpec
from wildpc_core.stacks import (
_declared_commands,
_migration_version,
available_actions,

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from pathlib import Path
from castle_core.secret_backends import (
from wildpc_core.secret_backends import (
FileSecretBackend,
OpenBaoBackend,
build_backend,
@@ -34,37 +34,37 @@ def test_file_backend_write_read_list_delete(tmp_path: Path) -> None:
def test_build_backend_defaults_to_file(tmp_path: Path, monkeypatch) -> None:
monkeypatch.delenv("CASTLE_SECRET_BACKEND", raising=False)
monkeypatch.delenv("WILDPC_SECRET_BACKEND", raising=False)
assert isinstance(build_backend(tmp_path), FileSecretBackend)
def test_build_backend_openbao_via_env(tmp_path: Path, monkeypatch) -> None:
monkeypatch.setenv("CASTLE_SECRET_BACKEND", "openbao")
monkeypatch.setenv("WILDPC_SECRET_BACKEND", "openbao")
assert isinstance(build_backend(tmp_path), OpenBaoBackend)
def test_build_backend_openbao_via_settings(tmp_path: Path, monkeypatch) -> None:
"""The castle.yaml `secrets:` block selects the backend (env still overrides)."""
monkeypatch.delenv("CASTLE_SECRET_BACKEND", raising=False)
settings = {"backend": "openbao", "addr": "https://vault:8200", "mount": "castle"}
"""The wildpc.yaml `secrets:` block selects the backend (env still overrides)."""
monkeypatch.delenv("WILDPC_SECRET_BACKEND", raising=False)
settings = {"backend": "openbao", "addr": "https://vault:8200", "mount": "wildpc"}
assert isinstance(build_backend(tmp_path, settings), OpenBaoBackend)
def test_openbao_unreachable_returns_none_no_fallback(tmp_path: Path) -> None:
"""No file fallback: an unreachable vault returns None even if a file exists."""
(tmp_path / "ONLY_IN_FILE").write_text("from-file")
backend = OpenBaoBackend(addr="http://127.0.0.1:1", token="dummy", mount="castle")
backend = OpenBaoBackend(addr="http://127.0.0.1:1", token="dummy", mount="wildpc")
assert backend.read("ONLY_IN_FILE") is None
assert backend.read("NOT_ANYWHERE") is None
def test_openbao_empty_token_returns_none(tmp_path: Path) -> None:
backend = OpenBaoBackend(addr="http://127.0.0.1:8200", token="", mount="castle")
backend = OpenBaoBackend(addr="http://127.0.0.1:8200", token="", mount="wildpc")
assert backend.read("K") is None
def test_openbao_node_prefix_from_settings(tmp_path: Path, monkeypatch) -> None:
monkeypatch.delenv("CASTLE_SECRET_BACKEND", raising=False)
monkeypatch.delenv("WILDPC_SECRET_BACKEND", raising=False)
backend = build_backend(
tmp_path,
{"backend": "openbao", "addr": "http://x", "node_prefix": "nodes/primer"},

View File

@@ -4,23 +4,23 @@ from __future__ import annotations
from pathlib import Path
from castle_core.generators.systemd import (
from wildpc_core.generators.systemd import (
generate_timer,
generate_unit_from_deployed,
secret_env_path,
unit_env_file,
unit_name,
)
from castle_core.registry import Deployment
from wildpc_core.registry import Deployment
class TestUnitName:
"""Tests for systemd unit naming."""
def test_unit_name_format(self) -> None:
"""Unit names follow castle-<name>.service pattern."""
assert unit_name("central-context") == "castle-central-context.service"
assert unit_name("my-svc") == "castle-my-svc.service"
"""Unit names follow wildpc-<name>.service pattern."""
assert unit_name("central-context") == "wildpc-central-context.service"
assert unit_name("my-svc") == "wildpc-my-svc.service"
class TestUnitFromDeployed:
@@ -35,7 +35,7 @@ class TestUnitFromDeployed:
description="Test service",
)
unit = generate_unit_from_deployed("test-svc", deployed)
assert "Description=Castle: Test service" in unit
assert "Description=Wild PC: Test service" in unit
def test_no_working_directory(self) -> None:
"""Unit file has no WorkingDirectory (source/runtime separation)."""
@@ -53,14 +53,14 @@ class TestUnitFromDeployed:
deployed = Deployment(
manager="systemd", launcher="python",
run_cmd=["/home/user/.local/bin/uv", "run", "test-svc"],
env={"TEST_SVC_DATA_DIR": "/home/user/.castle/data/test-svc"},
env={"TEST_SVC_DATA_DIR": "/home/user/.wildpc/data/test-svc"},
description="Test service",
)
unit = generate_unit_from_deployed("test-svc", deployed)
assert "Environment=TEST_SVC_DATA_DIR=/home/user/.castle/data/test-svc" in unit
assert "Environment=TEST_SVC_DATA_DIR=/home/user/.wildpc/data/test-svc" in unit
def test_default_path_emitted_when_absent(self) -> None:
"""Castle supplies a default PATH when the service doesn't pin one."""
"""Wild PC supplies a default PATH when the service doesn't pin one."""
deployed = Deployment(
manager="systemd", launcher="python",
run_cmd=["/home/user/.local/bin/uv", "run", "test-svc"],
@@ -99,7 +99,7 @@ class TestUnitFromDeployed:
assert unit.count("PATH=") == 1
def test_explicit_path_overrides_default(self) -> None:
"""A PATH pinned in defaults.env wins — Castle does not append its own,
"""A PATH pinned in defaults.env wins — Wild PC does not append its own,
which would clobber it under systemd's last-assignment-wins rule."""
deployed = Deployment(
manager="systemd", launcher="python",
@@ -140,15 +140,15 @@ class TestUnitFromDeployed:
run_cmd=["/home/user/.local/bin/uv", "run", "my-svc"],
env={
"MY_SVC_PORT": "9001",
"MY_SVC_DATA_DIR": "/home/user/.castle/data/my-svc",
"MY_SVC_DATA_DIR": "/home/user/.wildpc/data/my-svc",
},
description="My service",
)
unit = generate_unit_from_deployed("my-svc", deployed)
assert "Description=Castle: My service" in unit
assert "Description=Wild PC: My service" in unit
assert "ExecStart=/home/user/.local/bin/uv run my-svc" in unit
assert "Environment=MY_SVC_PORT=9001" in unit
assert "Environment=MY_SVC_DATA_DIR=/home/user/.castle/data/my-svc" in unit
assert "Environment=MY_SVC_DATA_DIR=/home/user/.wildpc/data/my-svc" in unit
assert "WorkingDirectory" not in unit
assert "Restart=on-failure" in unit
@@ -170,7 +170,7 @@ class TestUnitFromDeployed:
deployed = Deployment(
manager="systemd", launcher="python",
run_cmd=["/home/user/.local/bin/uv", "run", "my-svc"],
env={"DATA_DIR": "/home/user/.castle/data/my-svc"},
env={"DATA_DIR": "/home/user/.wildpc/data/my-svc"},
description="Test",
)
unit = generate_unit_from_deployed("my-svc", deployed)
@@ -180,12 +180,12 @@ class TestUnitFromDeployed:
"""A compose deployment's stop_cmd becomes ExecStop= (clean teardown)."""
deployed = Deployment(
manager="systemd", launcher="compose",
run_cmd=["/usr/bin/docker", "compose", "-p", "castle-x", "-f", "c.yml", "up"],
stop_cmd=["/usr/bin/docker", "compose", "-p", "castle-x", "-f", "c.yml", "down"],
run_cmd=["/usr/bin/docker", "compose", "-p", "wildpc-x", "-f", "c.yml", "up"],
stop_cmd=["/usr/bin/docker", "compose", "-p", "wildpc-x", "-f", "c.yml", "down"],
description="Stack",
)
unit = generate_unit_from_deployed("x", deployed)
assert "ExecStop=/usr/bin/docker compose -p castle-x -f c.yml down" in unit
assert "ExecStop=/usr/bin/docker compose -p wildpc-x -f c.yml down" in unit
def test_no_exec_stop_without_stop_cmd(self) -> None:
"""Runners without a stop_cmd rely on SIGTERM — no ExecStop line."""
@@ -206,7 +206,7 @@ class TestSecretEnvFile:
env={"PORT": "9001"},
secret_env_keys=["API_KEY"],
)
path = Path("/home/u/.castle/secrets/env/castle-my-svc.service.env")
path = Path("/home/u/.wildpc/secrets/env/wildpc-my-svc.service.env")
unit = generate_unit_from_deployed("my-svc", deployed, env_file=path)
assert f"EnvironmentFile={path}" in unit
# fail-loud: no '-' prefix
@@ -220,7 +220,7 @@ class TestSecretEnvFile:
schedule="0 2 * * *",
secret_env_keys=["TOKEN"],
)
path = Path("/home/u/.castle/secrets/env/castle-my-job.service.env")
path = Path("/home/u/.wildpc/secrets/env/wildpc-my-job.service.env")
unit = generate_unit_from_deployed("my-job", deployed, env_file=path)
assert "Type=oneshot" in unit
assert f"EnvironmentFile={path}" in unit
@@ -266,7 +266,7 @@ class TestGenerateTimer:
def test_daily_timer(self) -> None:
"""Daily cron produces OnCalendar timer."""
timer = generate_timer("my-job", schedule="0 2 * * *", description="Nightly")
assert "Description=Castle timer: Nightly" in timer
assert "Description=Wild PC timer: Nightly" in timer
assert "OnCalendar=*-*-* 02:00:00" in timer
assert "WantedBy=timers.target" in timer
@@ -279,4 +279,4 @@ class TestGenerateTimer:
def test_fallback_description(self) -> None:
"""Timer uses name when no description given."""
timer = generate_timer("my-job", schedule="0 0 * * *")
assert "Description=Castle timer: my-job" in timer
assert "Description=Wild PC timer: my-job" in timer

View File

@@ -1,4 +1,4 @@
"""Tests for castle-managed TLS material (core/src/castle_core/tls.py)."""
"""Tests for wildpc-managed TLS material (core/src/wildpc_core/tls.py)."""
from __future__ import annotations
@@ -6,9 +6,9 @@ from pathlib import Path
import pytest
import castle_core.config as C
import castle_core.tls as T
from castle_core.manifest import SystemdDeployment
import wildpc_core.config as C
import wildpc_core.tls as T
from wildpc_core.manifest import SystemdDeployment
def _write_wildcard(xdg: Path, domain: str, tag: str, acme_dir: str) -> None:
@@ -34,7 +34,7 @@ def tls_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
def _cfg(C, domain, dep, data_dir):
return C.CastleConfig(
return C.WildpcConfig(
root=None,
gateway=C.GatewayConfig(port=9000, domain=domain),
repo=None,

View File

@@ -1,4 +1,4 @@
"""Tests for castle_core.tool_schema — deriving neutral tool-call cores from --help."""
"""Tests for wildpc_core.tool_schema — deriving neutral tool-call cores from --help."""
from __future__ import annotations
@@ -6,7 +6,7 @@ from types import SimpleNamespace
import pytest
from castle_core.tool_schema import (
from wildpc_core.tool_schema import (
ToolSchemaError,
_command_core,
_extract_subcommands,
@@ -115,7 +115,7 @@ class TestDerive:
class TestLLMAssistHelpers:
"""Deterministic helpers that feed / validate the (castle-api) LLM assist."""
"""Deterministic helpers that feed / validate the (wildpc-api) LLM assist."""
def test_collect_tool_help_real_tool(self) -> None:
cfg = _fake_config({"python3": SimpleNamespace(source=None)})

View File

@@ -6,8 +6,8 @@ from pathlib import Path
import pytest
from castle_core import toolchains
from castle_core.toolchains import (
from wildpc_core import toolchains
from wildpc_core.toolchains import (
ToolchainError,
read_node_pin,
resolve_node_bin,
@@ -26,7 +26,7 @@ def _install(root: Path, *versions: str) -> None:
def nvm(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
root = tmp_path / "nvm"
root.mkdir()
monkeypatch.setenv("CASTLE_NODE_VERSIONS_DIR", str(root))
monkeypatch.setenv("WILDPC_NODE_VERSIONS_DIR", str(root))
return root
@@ -117,5 +117,5 @@ class TestResolveNodeBin:
resolve_node_bin(tmp_path)
def test_default_dir_is_nvm(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("CASTLE_NODE_VERSIONS_DIR", raising=False)
monkeypatch.delenv("WILDPC_NODE_VERSIONS_DIR", raising=False)
assert toolchains.node_versions_dir() == Path.home() / ".nvm" / "versions" / "node"

View File

@@ -4,11 +4,11 @@ from __future__ import annotations
import yaml
from castle_core.generators.tunnel import (
from wildpc_core.generators.tunnel import (
generate_tunnel_config,
public_hostnames,
)
from castle_core.registry import Deployment, NodeConfig, NodeRegistry
from wildpc_core.registry import Deployment, NodeConfig, NodeRegistry
def _registry(