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:
303
wildpc-api/tests/conftest.py
Normal file
303
wildpc-api/tests/conftest.py
Normal file
@@ -0,0 +1,303 @@
|
||||
"""Test fixtures for wildpc-api."""
|
||||
|
||||
import os as _os
|
||||
# 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("WILDPC_SECRET_BACKEND", "file")
|
||||
|
||||
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import wildpc_api.config as api_config
|
||||
from wildpc_api.main import app
|
||||
from wildpc_core.registry import (
|
||||
Deployment,
|
||||
NodeConfig,
|
||||
NodeRegistry,
|
||||
save_registry,
|
||||
)
|
||||
|
||||
|
||||
def _free_port() -> int:
|
||||
s = socket.socket()
|
||||
s.bind(("", 0))
|
||||
port = s.getsockname()[1]
|
||||
s.close()
|
||||
return port
|
||||
|
||||
|
||||
def _docker_available() -> bool:
|
||||
try:
|
||||
return subprocess.run(
|
||||
["docker", "info"], capture_output=True, timeout=5
|
||||
).returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def nats_url() -> Generator[str, None, None]:
|
||||
"""A throwaway NATS+JetStream broker in docker (fresh per test for clean
|
||||
buckets). Skips if docker is unavailable."""
|
||||
if not _docker_available():
|
||||
pytest.skip("docker unavailable — skipping NATS integration tests")
|
||||
cport, mport = _free_port(), _free_port()
|
||||
name = f"wildpc-test-nats-{cport}"
|
||||
subprocess.run(
|
||||
["docker", "run", "-d", "--rm", "--name", name,
|
||||
"-p", f"{cport}:4222", "-p", f"{mport}:8222", "nats:2", "-js", "-m", "8222"],
|
||||
check=True, capture_output=True,
|
||||
)
|
||||
try:
|
||||
for _ in range(50): # wait for readiness
|
||||
try:
|
||||
urllib.request.urlopen(f"http://127.0.0.1:{mport}/healthz", timeout=1)
|
||||
break
|
||||
except Exception:
|
||||
time.sleep(0.2)
|
||||
yield f"nats://127.0.0.1:{cport}"
|
||||
finally:
|
||||
subprocess.run(["docker", "rm", "-f", name], capture_output=True)
|
||||
|
||||
|
||||
def _modernize_deployment(spec: dict) -> dict:
|
||||
"""Translate a test's terse legacy deployment dict to the current
|
||||
manager-discriminated shape (production dropped this read-compat post-migration).
|
||||
``proxy``/``public`` → ``reach``; ``run.runner`` → ``manager`` (+ ``run.launcher``)."""
|
||||
d = dict(spec)
|
||||
proxy = bool(d.pop("proxy", False))
|
||||
public = bool(d.pop("public", False))
|
||||
if "reach" not in d:
|
||||
if public:
|
||||
d["reach"] = "public"
|
||||
elif proxy:
|
||||
d["reach"] = "internal"
|
||||
if "manager" not in d:
|
||||
run = dict(d.pop("run", None) or {})
|
||||
runner = run.get("runner")
|
||||
if runner == "static":
|
||||
d["manager"] = "caddy"
|
||||
if run.get("root"):
|
||||
d["root"] = run["root"]
|
||||
elif runner == "path":
|
||||
d["manager"] = "path"
|
||||
elif runner == "remote":
|
||||
d["manager"] = "none"
|
||||
for k in ("base_url", "health_url"):
|
||||
if run.get(k):
|
||||
d[k] = run[k]
|
||||
else:
|
||||
launch = {k: v for k, v in run.items() if k != "runner"}
|
||||
launch["launcher"] = runner
|
||||
d["manager"] = "systemd"
|
||||
d["run"] = launch
|
||||
return d
|
||||
|
||||
|
||||
def _store_for(spec: dict) -> str:
|
||||
if spec.get("schedule"):
|
||||
return "jobs"
|
||||
return {"systemd": "services", "caddy": "statics", "path": "tools", "none": "references"}[
|
||||
spec["manager"]
|
||||
]
|
||||
|
||||
|
||||
def _write_wildpc_config(root: Path, config: dict) -> None:
|
||||
"""Scatter a nested wildpc config dict into the on-disk layout: wildpc.yaml globals,
|
||||
programs/<name>.yaml, and deployments/<kind>/<name>.yaml (fields modernized)."""
|
||||
globals_data = {k: v for k, v in config.items() if k in ("gateway", "repo")}
|
||||
(root / "wildpc.yaml").write_text(yaml.dump(globals_data, default_flow_style=False))
|
||||
programs = config.get("programs") or {}
|
||||
if programs:
|
||||
(root / "programs").mkdir(parents=True, exist_ok=True)
|
||||
for name, spec in programs.items():
|
||||
(root / "programs" / f"{name}.yaml").write_text(
|
||||
yaml.dump(spec, default_flow_style=False)
|
||||
)
|
||||
for section in ("services", "jobs", "deployments"):
|
||||
for name, spec in (config.get(section) or {}).items():
|
||||
modern = _modernize_deployment(spec)
|
||||
store_dir = root / "deployments" / _store_for(modern)
|
||||
store_dir.mkdir(parents=True, exist_ok=True)
|
||||
(store_dir / f"{name}.yaml").write_text(
|
||||
yaml.dump(modern, default_flow_style=False)
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def wildpc_root(tmp_path: Path) -> Generator[Path, None, None]:
|
||||
"""Create a temporary wildpc root with directory-per-resource config."""
|
||||
config = {
|
||||
"gateway": {"port": 9000},
|
||||
"programs": {
|
||||
"test-tool": {
|
||||
"description": "Test tool",
|
||||
"source": "test-tool",
|
||||
"system_dependencies": ["pandoc"],
|
||||
},
|
||||
"test-tool-2": {
|
||||
"description": "Another test tool",
|
||||
"source": "test-tool-2",
|
||||
"version": "2.0.0",
|
||||
},
|
||||
"wired-in": {
|
||||
"description": "Adopted repo, no stack",
|
||||
"source": "wired-in",
|
||||
"repo": "https://github.com/someone/wired-in.git",
|
||||
"commands": {
|
||||
"lint": [["make", "lint"]],
|
||||
"test": [["make", "test"]],
|
||||
"run": [["./bin/wired-in"]],
|
||||
},
|
||||
},
|
||||
},
|
||||
"services": {
|
||||
# Path deployments — behavior "tool" derives from the `path` runner
|
||||
# (behavior is derived from deployments, never stored).
|
||||
"test-tool": {"program": "test-tool", "run": {"runner": "path"}},
|
||||
"test-tool-2": {"program": "test-tool-2", "run": {"runner": "path"}},
|
||||
"wired-in": {"program": "wired-in", "run": {"runner": "path"}},
|
||||
"test-svc": {
|
||||
"program": "test-svc-comp",
|
||||
"description": "Test service",
|
||||
"run": {
|
||||
"runner": "python",
|
||||
"program": "test-svc",
|
||||
},
|
||||
"expose": {
|
||||
"http": {
|
||||
"internal": {"port": 19000},
|
||||
"health_path": "/health",
|
||||
}
|
||||
},
|
||||
"proxy": True,
|
||||
"manage": {"systemd": {}},
|
||||
},
|
||||
},
|
||||
"jobs": {
|
||||
"test-job": {
|
||||
"description": "Test job",
|
||||
"run": {
|
||||
"runner": "command",
|
||||
"argv": ["test-job"],
|
||||
},
|
||||
"schedule": "0 2 * * *",
|
||||
},
|
||||
},
|
||||
}
|
||||
_write_wildpc_config(tmp_path, config)
|
||||
yield tmp_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def registry_path(tmp_path: Path, wildpc_root: Path) -> Generator[Path, None, None]:
|
||||
"""Create a temporary registry.yaml and patch the module to use it."""
|
||||
reg_path = tmp_path / "registry.yaml"
|
||||
registry = NodeRegistry(
|
||||
node=NodeConfig(
|
||||
hostname="test-node",
|
||||
wildpc_root=str(wildpc_root),
|
||||
gateway_port=9000,
|
||||
),
|
||||
deployed={
|
||||
NodeRegistry.key("service", "test-svc"): Deployment(
|
||||
manager="systemd",
|
||||
launcher="python",
|
||||
run_cmd=["uv", "run", "test-svc"],
|
||||
env={
|
||||
"TEST_SVC_PORT": "19000",
|
||||
"TEST_SVC_DATA_DIR": "/home/user/.wildpc/data/test-svc",
|
||||
},
|
||||
description="Test service",
|
||||
name="test-svc",
|
||||
kind="service",
|
||||
port=19000,
|
||||
health_path="/health",
|
||||
subdomain="test-svc",
|
||||
managed=True,
|
||||
),
|
||||
# A deployed tool (path) — must NOT leak into the /services list.
|
||||
NodeRegistry.key("tool", "test-tool"): Deployment(
|
||||
manager="path",
|
||||
run_cmd=[],
|
||||
description="Test tool",
|
||||
name="test-tool",
|
||||
kind="tool",
|
||||
),
|
||||
},
|
||||
)
|
||||
save_registry(registry, reg_path)
|
||||
|
||||
# Patch the registry path and helper functions
|
||||
import wildpc_core.registry as reg_mod
|
||||
import wildpc_api.routes as routes_mod
|
||||
import wildpc_api.services as services_mod
|
||||
import wildpc_api.nodes as nodes_mod
|
||||
import wildpc_api.stream as stream_mod
|
||||
import wildpc_api.config_editor as config_editor_mod
|
||||
|
||||
original_path = reg_mod.REGISTRY_PATH
|
||||
reg_mod.REGISTRY_PATH = reg_path
|
||||
|
||||
def _get_registry() -> NodeRegistry:
|
||||
from wildpc_core.registry import load_registry
|
||||
|
||||
return load_registry(reg_path)
|
||||
|
||||
def _get_wildpc_root() -> Path | None:
|
||||
return wildpc_root
|
||||
|
||||
# Save originals and patch everywhere these are imported
|
||||
originals = {
|
||||
"api_config.get_registry": api_config.get_registry,
|
||||
"api_config.get_wildpc_root": api_config.get_wildpc_root,
|
||||
"routes.get_registry": routes_mod.get_registry,
|
||||
"routes.get_wildpc_root": routes_mod.get_wildpc_root,
|
||||
"services.get_registry": services_mod.get_registry,
|
||||
"services.get_wildpc_root": services_mod.get_wildpc_root,
|
||||
"nodes.get_registry": nodes_mod.get_registry,
|
||||
"stream.get_registry": stream_mod.get_registry,
|
||||
"config_editor.get_wildpc_root": config_editor_mod.get_wildpc_root,
|
||||
}
|
||||
|
||||
for mod in [
|
||||
api_config,
|
||||
routes_mod,
|
||||
services_mod,
|
||||
nodes_mod,
|
||||
stream_mod,
|
||||
config_editor_mod,
|
||||
]:
|
||||
if hasattr(mod, "get_registry"):
|
||||
mod.get_registry = _get_registry
|
||||
if hasattr(mod, "get_wildpc_root"):
|
||||
mod.get_wildpc_root = _get_wildpc_root
|
||||
|
||||
yield reg_path
|
||||
|
||||
reg_mod.REGISTRY_PATH = original_path
|
||||
api_config.get_registry = originals["api_config.get_registry"]
|
||||
api_config.get_wildpc_root = originals["api_config.get_wildpc_root"]
|
||||
routes_mod.get_registry = originals["routes.get_registry"]
|
||||
routes_mod.get_wildpc_root = originals["routes.get_wildpc_root"]
|
||||
services_mod.get_registry = originals["services.get_registry"]
|
||||
services_mod.get_wildpc_root = originals["services.get_wildpc_root"]
|
||||
nodes_mod.get_registry = originals["nodes.get_registry"]
|
||||
stream_mod.get_registry = originals["stream.get_registry"]
|
||||
config_editor_mod.get_wildpc_root = originals["config_editor.get_wildpc_root"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(registry_path: Path) -> Generator[TestClient, None, None]:
|
||||
"""Create a test client with temporary registry."""
|
||||
with TestClient(app) as client:
|
||||
yield client
|
||||
Reference in New Issue
Block a user