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:
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"},
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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": {},
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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}}},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)})
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user