Files
serverAppConfigs/tests/homepage_config.bats
T
Frank bad78079d7 feat(homepage): the not-open-yet apps get their own section, below AI
Calibre-Web, RomM and Seerr sat inside "Media Library Apps" carrying a WIP
badge and a "not open to family users yet" sentence each, so the household
grid mixed doors they can use with doors they cannot. They now render under
their own "Work in progress" heading, placed after AI on the user page, and
the copy drops the restated "Work in progress:" for the ask-Frank line the
section heading no longer needs to repeat.

- app.js: SECTION_ORDER.user gains "Work in progress" last.
- services.json: the three cards' section, plus one-line copy trim.
- homepage_config.bats / homepage_pages.bats: the section contract now
  includes the new heading and pins the new order (the old test pinned AI
  last, which this change deliberately overturns).
- Homepage.md: the classification list and the SECTION_ORDER/wip contract
  lines catch up (the user order in the page was already stale, and "down"
  was an undocumented wip value).

The admin page is untouched: its WIP cards (Gluetun, mariadb, valkey,
Postgres) are plumbing inside Infrastructure, not applications.
2026-11-04 13:09:16 -08:00

137 lines
6.7 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", "Work in progress"},
"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"
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"
}