All checks were successful
plugin-tests / test (push) Successful in 25s
Lands the easy migrations + the automation skeleton. STT migration deferred
to Phase 2.1 (it touches the streaming engine + bootstrap JS — needs a new
streaming_hook public-API method OR forced-internal CONNECTION-MAP entries).
Migrated to plugin:
routes/vault_status.py GET /api/vault/status (from fork commit 3e2c74f3)
static/{app.js,app.css,fonts/} brand skin (from hermes-ext/)
Plugin auto-loaded by hermes-webui when HERMES_WEBUI_PYTHON_PLUGIN is set;
register_static + inject_stylesheet + inject_script wire the URL contract at
/plugins/svrnty/{app.css,app.js} per protocol §14 (Q5).
Automation skeleton:
Makefile one-liner targets: test · map · sync-upstream · smoke
scripts/boot-smoke.py start upstream+plugin, curl every endpoint
scripts/upstream-sync.py fetch tags + run matrix + JSON report
tests/evals/test_features.py 4 evals (loader contract · vault payload · brand URL contract · forced-internal=0)
tests/unit/test_brand_skin.py 4 asset-presence + wiring tests
tests/unit/test_vault_status.py 3 handler tests (register, success, error)
CONNECTION-MAP.md: 0 forced-internal dependencies; plugin uses only public API.
AST script timestamp removed so map-check is deterministic.
Tests: 11/11 PASS (4 evals + 7 unit). Integration tests deferred until
boot-smoke runs against a live hermes-webui (Phase 2.D + 2.E gate).
Deferred to next session:
P2.A STT migration (needs streaming_hook design — see routes/transcribe.py)
P2.D Revert 4 fork feature commits — needs STT migration first
P2.E Archive hermes-ext repo — gated on P2.D
P2.F Live boot smoke against real webui
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
134 lines
4.2 KiB
Python
Executable File
134 lines
4.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""boot-smoke.py — start hermes-webui + plugin, curl every plugin endpoint.
|
|
|
|
Exit 0 if every endpoint returns its expected status, 1 otherwise. Used by
|
|
upstream-sync.py and as a one-shot manual check after install.
|
|
|
|
Usage:
|
|
python3 boot-smoke.py # start webui + smoke + stop
|
|
python3 boot-smoke.py --no-start # webui already running; just smoke
|
|
python3 boot-smoke.py --base http://... # smoke against custom base URL
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import signal
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from urllib.request import Request, urlopen
|
|
from urllib.error import URLError
|
|
|
|
PLUGIN_REPO = Path(__file__).resolve().parent.parent
|
|
|
|
# Endpoints we expect after the plugin is loaded. Status codes and content
|
|
# checks are minimal — this is "did it boot", not "is it correct".
|
|
SMOKE = [
|
|
{"path": "/healthz", "expect": [200], "kind": "vanilla"},
|
|
{"path": "/api/vault/status", "expect": [200, 401, 403], "kind": "plugin"},
|
|
{"path": "/plugins/svrnty/app.css", "expect": [200], "kind": "plugin-static"},
|
|
{"path": "/plugins/svrnty/app.js", "expect": [200], "kind": "plugin-static"},
|
|
]
|
|
|
|
|
|
def _free_port():
|
|
s = socket.socket()
|
|
s.bind(("127.0.0.1", 0))
|
|
port = s.getsockname()[1]
|
|
s.close()
|
|
return port
|
|
|
|
|
|
def _wait_for(url, timeout=20):
|
|
deadline = time.time() + timeout
|
|
while time.time() < deadline:
|
|
try:
|
|
with urlopen(url, timeout=1) as r:
|
|
if r.status < 500:
|
|
return True
|
|
except URLError:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
time.sleep(0.3)
|
|
return False
|
|
|
|
|
|
def _hit(base, path):
|
|
url = base.rstrip("/") + path
|
|
try:
|
|
with urlopen(url, timeout=5) as r:
|
|
return r.status, r.read()[:200]
|
|
except URLError as e:
|
|
if hasattr(e, "code"):
|
|
return e.code, b""
|
|
return None, str(e).encode()
|
|
except Exception as e:
|
|
return None, str(e).encode()
|
|
|
|
|
|
def smoke(base):
|
|
rows = []
|
|
failed = 0
|
|
for s in SMOKE:
|
|
status, _body = _hit(base, s["path"])
|
|
ok = status in s["expect"]
|
|
rows.append({"path": s["path"], "status": status, "kind": s["kind"], "ok": ok})
|
|
if not ok:
|
|
failed += 1
|
|
return rows, failed
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
ap.add_argument("--no-start", action="store_true",
|
|
help="Assume hermes-webui is already running; just curl")
|
|
ap.add_argument("--base", default=None,
|
|
help="Base URL (default http://127.0.0.1:<port>)")
|
|
ap.add_argument("--webui-dir",
|
|
default=str(PLUGIN_REPO.parent / "hermes-webui"),
|
|
help="Path to hermes-webui repo")
|
|
args = ap.parse_args()
|
|
|
|
proc = None
|
|
base = args.base or "http://127.0.0.1:8787"
|
|
|
|
if not args.no_start:
|
|
port = _free_port()
|
|
base = f"http://127.0.0.1:{port}"
|
|
env = os.environ.copy()
|
|
env["HERMES_WEBUI_PYTHON_PLUGIN"] = "svrnty_hermes_webui_plugin"
|
|
env["PORT"] = str(port)
|
|
# Best-effort: start under the agent venv if it exists; else system python.
|
|
py = Path(args.webui_dir) / "venv" / "bin" / "python"
|
|
cmd = [str(py) if py.exists() else "python3", "bootstrap.py", "--foreground"]
|
|
proc = subprocess.Popen(
|
|
cmd, cwd=args.webui_dir, env=env,
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
preexec_fn=os.setsid,
|
|
)
|
|
if not _wait_for(f"{base}/healthz", timeout=30):
|
|
print(f"FAIL: webui did not respond at {base}/healthz within 30s",
|
|
file=sys.stderr)
|
|
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
|
sys.exit(1)
|
|
|
|
try:
|
|
rows, failed = smoke(base)
|
|
finally:
|
|
if proc is not None:
|
|
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
|
try:
|
|
proc.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
|
|
|
print(json.dumps({"base": base, "rows": rows, "failed": failed}, indent=2))
|
|
sys.exit(0 if failed == 0 else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|