- card/docs: prism-ml payload link, PQ2_0 spec, ~77 t/s (measured single stream, replacing the 9B's ~95) - warning re-keyed: zealot is no longer an uncensored endpoint, so the card carries the base-alignment notice and points uncensored work at dragoon; the test's "common uncensored text" equality (zealot==dragoon) is retired with the payload it described — dragoon alone carries the legal warning
139 lines
6.8 KiB
Bash
139 lines
6.8 KiB
Bash
#!/usr/bin/env bats
|
|
# Seam: shared/homepage/services.json — the service config the checker reads
|
|
# and the pages render. It must be a JSON list; every entry carries name,
|
|
# category, section, and a host:port probe. category is user or admin;
|
|
# section must be one the page's SECTION_ORDER renders. method, when
|
|
# present, is tcp or http. wip, when set, is "open", "closed" or "down".
|
|
|
|
load helpers
|
|
|
|
@test "services.json: valid schema" {
|
|
python3 - "$REPO_ROOT/shared/homepage/services.json" <<'PY'
|
|
import json, sys
|
|
path = sys.argv[1]
|
|
try:
|
|
data = json.load(open(path))
|
|
except FileNotFoundError:
|
|
sys.exit("services.json not found")
|
|
assert isinstance(data, list), "top level must be a list"
|
|
assert data, "the list must not be empty"
|
|
for svc in data:
|
|
for field in ("name", "category", "section", "probe"):
|
|
assert field in svc, f"{svc.get('name', '?')} is missing {field}"
|
|
assert svc["category"] in ("user", "admin"), svc["category"]
|
|
if "method" in svc:
|
|
assert svc["method"] in ("tcp", "http"), svc["method"]
|
|
wip = svc.get("wip")
|
|
assert wip is False or wip in ("open", "closed", "down"), f"{svc['name']} wip must be false, 'open', 'closed' or 'down'"
|
|
host, _, port = svc["probe"].rpartition(":")
|
|
assert host and port.isdigit(), f"bad probe {svc['probe']}"
|
|
PY
|
|
}
|
|
|
|
@test "services.json: every section is one the page renders" {
|
|
# A service whose section is not in app.js's SECTION_ORDER for its
|
|
# category is silently dropped from the page. Pin the contract.
|
|
python3 - "$REPO_ROOT/shared/homepage/services.json" <<'PY'
|
|
import json, sys
|
|
data = json.load(open(sys.argv[1]))
|
|
allowed = {
|
|
"user": {"AI", "Web Apps", "Media Library Apps"},
|
|
"admin": {"Media Library Apps", "Observability", "AI", "Infrastructure"},
|
|
}
|
|
for svc in data:
|
|
assert svc.get("section") in allowed[svc["category"]], (
|
|
f"{svc['name']} has unrenderable section {svc.get('section')!r} for {svc['category']}")
|
|
PY
|
|
}
|
|
|
|
@test "services.json: every user service has an Open link, status-only backends do not" {
|
|
python3 - "$REPO_ROOT/shared/homepage/services.json" <<'PY'
|
|
import json, sys
|
|
data = json.load(open(sys.argv[1]))
|
|
for svc in data:
|
|
show = svc.get("showLink", True)
|
|
if show:
|
|
assert svc.get("url"), f"{svc['name']} shows a link but has no url"
|
|
else:
|
|
assert not svc.get("url"), f"{svc['name']} is status-only but has a url"
|
|
PY
|
|
}
|
|
|
|
@test "the LLM status cards track the fleet, not retired service names" {
|
|
# This drifted twice: the card probing Dankserver's :7770 was still named
|
|
# frank-small long after frank-medium took that endpoint; the endpoints
|
|
# were renamed to the Protoss set on 2026-10-08. The probe target is what
|
|
# matters; the name has to follow it.
|
|
python3 - "$REPO_ROOT/shared/homepage/services.json" <<'PY'
|
|
import json, sys
|
|
services = json.load(open(sys.argv[1]))
|
|
by_probe = {s["probe"]: s["name"] for s in services}
|
|
assert by_probe.get("192.168.0.235:7770") == "frank-archon", by_probe.get("192.168.0.235:7770")
|
|
assert by_probe.get("127.0.0.1:7770") == "frank-dragoon", by_probe.get("127.0.0.1:7770")
|
|
assert by_probe.get("127.0.0.1:7771") == "frank-zealot", by_probe.get("127.0.0.1:7771")
|
|
PY
|
|
}
|
|
|
|
@test "the Model Information component is complete: text, art, voices, warnings" {
|
|
# The fused model panel is the family's only description of the fleet.
|
|
# Every model card must carry: equivalence note, supervision level, the
|
|
# unit art, at least one playable voice line, and (for uncensored models)
|
|
# the legal warning. The compaction recommendation must name the zealot.
|
|
python3 - "$REPO_ROOT/shared/homepage/services.json" "$REPO_ROOT/shared/homepage" <<'PY'
|
|
import json, os, sys
|
|
services = json.load(open(sys.argv[1]))
|
|
root = sys.argv[2]
|
|
fleet = [s for s in services if "model" in s]
|
|
assert {s["name"] for s in fleet} == {"frank-dragoon", "frank-archon", "frank-zealot"}, [s["name"] for s in fleet]
|
|
for s in fleet:
|
|
m = s["model"]
|
|
assert m["equiv"].strip(), f"{s['name']} has no equivalence note"
|
|
assert m["supervision"].strip(), f"{s['name']} has no supervision note"
|
|
assert m["sfx"], f"{s['name']} has no voice line"
|
|
assert os.path.isfile(os.path.join(root, "img", "unit-" + m["unit"] + ".jpg")), f"{s['name']} art missing"
|
|
for f in m["sfx"] + m.get("voice", {}).get("sfx", []):
|
|
assert os.path.isfile(os.path.join(root, "sfx", f)), f"{s['name']} voice {f} missing"
|
|
zealot = next(s for s in fleet if s["name"] == "frank-zealot")
|
|
assert "compaction" in zealot["model"].get("compaction", ""), "zealot must be recommended as compaction model"
|
|
assert zealot["model"].get("compaction", "") != "", "compaction advice must be its own visible field"
|
|
assert "supervision" in zealot["model"]["supervision"].lower()
|
|
for s in fleet:
|
|
m = s["model"]
|
|
uncensored = "ncensored" in m["spec"] or "bliterated" in m["spec"]
|
|
if uncensored:
|
|
assert "legal" in m.get("warn", "").lower(), f"{s['name']} is uncensored but has no legal warning"
|
|
assert [s["name"] for s in fleet] == ["frank-zealot", "frank-dragoon", "frank-archon"], \
|
|
"card render order must be zealot, dragoon, archon: " + str([s["name"] for s in fleet])
|
|
w = {s["name"]: s["model"].get("warn", "") for s in fleet}
|
|
assert "legal risk" in w["frank-dragoon"].lower() and "everybody" in w["frank-dragoon"].lower(), \
|
|
"the abliterated endpoint must carry the common legal warning"
|
|
assert "censored" in w["frank-zealot"].lower() and "uncensored" not in w["frank-zealot"].lower(), \
|
|
"zealot ships the censored base since 2026-10-17: its card warning must not claim uncensored output"
|
|
archon = next(s for s in fleet if s["name"] == "frank-archon")
|
|
assert "permission" in archon["model"], "archon card needs the ask-Frank permission callout"
|
|
PY
|
|
grep -q "renderModelInfo" "$REPO_ROOT/shared/homepage/app.js"
|
|
grep -q ".mi-card" "$REPO_ROOT/shared/homepage/style.css"
|
|
}
|
|
|
|
@test "every app card carries icon art and SearXNG carries the privacy note" {
|
|
# The family sees unit-portrait art on every card, not just the models:
|
|
# brand marks where Simple Icons has them, improvised monograms otherwise.
|
|
# SearXNG's card must warn (hover) that obfuscation is not anonymity.
|
|
python3 - "$REPO_ROOT/shared/homepage/services.json" "$REPO_ROOT/shared/homepage" <<'PY'
|
|
import json, os, sys
|
|
services = json.load(open(sys.argv[1]))
|
|
root = sys.argv[2]
|
|
apps = [s for s in services if "model" not in s]
|
|
for s in apps:
|
|
art = s.get("art", "")
|
|
assert art.startswith("/img/apps/"), f"{s['name']} has no app art"
|
|
assert os.path.isfile(os.path.join(root, art.lstrip("/"))), f"{s['name']} art file missing: {art}"
|
|
sx = next(s for s in services if s["name"] == "SearXNG")
|
|
assert "anonymous" in sx.get("warn", "").lower(), "SearXNG must warn that searches are obfuscated, not anonymous"
|
|
PY
|
|
grep -q "app-art" "$REPO_ROOT/shared/homepage/app.js"
|
|
grep -q ".app-art" "$REPO_ROOT/shared/homepage/style.css"
|
|
grep -q "/whoami" "$REPO_ROOT/dankclaw/Caddyfile"
|
|
}
|