Rename Castle -> Wild PC across the repo

Repo-side rename only (Phases 1-3 of the migration plan); the live box
(~/.castle, systemd units, /data/castle, domains) is a separate cutover.

- Slug `castle` -> `wildpc`: CLI command, module names (wildpc_core/cli/api),
  dist names, entry point `wildpc = wildpc_cli.main:main`.
- Identifiers: CastleConfig/NATSClient/DirError/MDNS -> Wildpc*.
- Env/constants: CASTLE_* -> WILDPC_*; ~/.castle -> ~/.wildpc, castle.yaml ->
  wildpc.yaml, /data/castle -> /data/wildpc.
- Systemd UNIT_PREFIX castle- -> wildpc-; own programs castle-api/gateway/etc.
- Display prose "Castle" -> "Wild PC" in docs, agent-guide files, README, frontend.
- Package dirs and bootstrap yaml renamed via git mv; lockfiles regenerated;
  redundant nested uv.lock files dropped (workspace root lock is authoritative).

Tests: core 273, cli 47, wildpc-api 120 all pass. Frontend type-checks + builds.
Fixed a stale test fixture (secret_env_path kind arg) broken pre-rename.
This commit is contained in:
2026-07-18 22:55:08 -07:00
parent 25d7a522cc
commit 05b28cb584
190 changed files with 2428 additions and 3337 deletions

View File

@@ -0,0 +1,199 @@
"""POST /config/tools/{name}/schema — derive an Anthropic tool schema from --help,
and the tool_schema field round-tripping through the tool save path."""
from __future__ import annotations
from pathlib import Path
from fastapi.testclient import TestClient
import json
from wildpc_core.config import load_config
from wildpc_api.config import settings
def _completion(args: dict) -> dict:
"""A minimal chat-completions response carrying an emit_tool_schema call."""
return {
"choices": [
{"message": {"tool_calls": [
{"function": {"name": "emit_tool_schema", "arguments": json.dumps(args)}}
]}}
]
}
_BAD_CORE = {
"name": "python3", "description": "d",
"parameters": {"type": "object", "properties": {"m": {"type": "string", "enum": 3}}},
}
_GOOD_CORE = {
"name": "python3", "description": "d",
"parameters": {"type": "object", "properties": {"m": {"type": "string"}}},
}
# A tool whose program falls back to the tool name for its executable; `python3`
# is always on PATH, so derive succeeds deterministically.
_PY_TOOL = {"program": "python3", "manager": "path"}
class TestGenerateSchema:
def test_generate_returns_draft_not_saved(
self, client: TestClient, wildpc_root: Path
) -> None:
client.put("/config/tools/python3", json={"config": _PY_TOOL})
r = client.post("/config/tools/python3/schema")
assert r.status_code == 200, r.text
body = r.json()
schema = body["schema"]
# The stored draft is the neutral core (name/description/parameters),
# rendered to a provider envelope only on read.
assert schema["name"] == "python3"
assert "parameters" in schema
assert schema["parameters"]["properties"]
# It's a draft: nothing persisted until the client saves it.
cfg = load_config(wildpc_root)
assert cfg.tools["python3"].tool_schema is None
def test_unknown_tool_404(self, client: TestClient) -> None:
r = client.post("/config/tools/nope-not-a-tool/schema")
assert r.status_code == 404
def test_validate_endpoint_valid(self, client: TestClient) -> None:
r = client.post("/config/tools/schema/validate", json=_GOOD_CORE)
assert r.status_code == 200
assert r.json() == {"valid": True, "errors": []}
def test_validate_endpoint_invalid(self, client: TestClient) -> None:
r = client.post("/config/tools/schema/validate", json=_BAD_CORE)
assert r.status_code == 200
body = r.json()
assert body["valid"] is False and body["errors"]
def test_uninstalled_executable_422(
self, client: TestClient, wildpc_root: Path
) -> None:
client.put(
"/config/tools/ghosttool",
json={"config": {"program": "ghosttool", "manager": "path"}},
)
r = client.post("/config/tools/ghosttool/schema")
assert r.status_code == 422
assert "PATH" in r.json()["detail"]
def test_assist_disabled_returns_503(
self, client: TestClient, wildpc_root: Path
) -> None:
client.put("/config/tools/python3", json={"config": _PY_TOOL})
# llm_enabled is False by default.
r = client.post("/config/tools/python3/schema?assist=llm")
assert r.status_code == 503
assert "disabled" in r.json()["detail"]
def test_assist_success_returns_draft(
self, client: TestClient, wildpc_root: Path, monkeypatch
) -> None:
client.put("/config/tools/python3", json={"config": _PY_TOOL})
async def _fake_llm(help_text: str, name: str) -> dict:
assert "python3" in help_text # the recursive help was gathered
return {
"name": name,
"description": "the python interpreter",
"parameters": {
"type": "object",
"properties": {"script": {"type": "string"}},
},
}
monkeypatch.setattr(settings, "llm_enabled", True)
monkeypatch.setattr("wildpc_api.llm.generate_tool_schema_llm", _fake_llm)
r = client.post("/config/tools/python3/schema?assist=llm")
assert r.status_code == 200, r.text
body = r.json()
assert body["assist"] == "llm"
assert body["schema"]["parameters"]["properties"] == {"script": {"type": "string"}}
def test_assist_upstream_error_returns_502(
self, client: TestClient, wildpc_root: Path, monkeypatch
) -> None:
client.put("/config/tools/python3", json={"config": _PY_TOOL})
async def _fail_llm(help_text: str, name: str) -> dict:
from wildpc_api.llm import LLMAssistError
raise LLMAssistError("litellm returned 500")
monkeypatch.setattr(settings, "llm_enabled", True)
monkeypatch.setattr("wildpc_api.llm.generate_tool_schema_llm", _fail_llm)
r = client.post("/config/tools/python3/schema?assist=llm")
assert r.status_code == 502
assert "litellm" in r.json()["detail"]
def test_assist_repairs_invalid_then_valid(
self, client: TestClient, wildpc_root: Path, monkeypatch
) -> None:
"""A malformed first response (bad enum) is fed back and repaired."""
client.put("/config/tools/python3", json={"config": _PY_TOOL})
calls = {"n": 0}
async def _fake_complete(messages: list, key: str) -> dict:
calls["n"] += 1
return _completion(_BAD_CORE if calls["n"] == 1 else _GOOD_CORE)
monkeypatch.setattr(settings, "llm_enabled", True)
monkeypatch.setattr("wildpc_api.llm.read_secret", lambda name: "k")
monkeypatch.setattr("wildpc_api.llm._complete", _fake_complete)
r = client.post("/config/tools/python3/schema?assist=llm")
assert r.status_code == 200, r.text
assert calls["n"] == 2 # repaired on the second attempt
props = r.json()["schema"]["parameters"]["properties"]
assert props == {"m": {"type": "string"}}
def test_assist_unrepairable_returns_502(
self, client: TestClient, wildpc_root: Path, monkeypatch
) -> None:
"""Persistently invalid output exhausts the repair budget → 502."""
client.put("/config/tools/python3", json={"config": _PY_TOOL})
async def _always_bad(messages: list, key: str) -> dict:
return _completion(_BAD_CORE)
monkeypatch.setattr(settings, "llm_enabled", True)
monkeypatch.setattr("wildpc_api.llm.read_secret", lambda name: "k")
monkeypatch.setattr("wildpc_api.llm._complete", _always_bad)
r = client.post("/config/tools/python3/schema?assist=llm")
assert r.status_code == 502
assert "attempts" in r.json()["detail"]
def test_schema_round_trips_through_save(
self, client: TestClient, wildpc_root: Path
) -> None:
"""Saving a tool with tool_schema persists it (PATCH merge), and clearing
it with null drops the field."""
client.put("/config/tools/python3", json={"config": _PY_TOOL})
schema = client.post("/config/tools/python3/schema").json()["schema"]
client.put(
"/config/tools/python3",
json={"config": {"program": "python3", "tool_schema": schema}},
)
cfg = load_config(wildpc_root)
assert cfg.tools["python3"].tool_schema is not None
assert cfg.tools["python3"].tool_schema["name"] == "python3"
# Clearing with null removes it (default None).
client.put(
"/config/tools/python3",
json={"config": {"program": "python3", "tool_schema": None}},
)
cfg = load_config(wildpc_root)
assert cfg.tools["python3"].tool_schema is None