The coordinator already existed on the control computer and its contract is frozen, so this is the classroom half of a contract that was read from its README and source rather than guessed: four fields out, the status topic subscribed before anything is published, and every word the teacher reads taken from the coordinator's own message field. It is a separate process, not a Plasma applet. A broker library, a network timeout and a text entry have no business inside the only way a teacher has of getting anywhere; if this dies the bar is untouched. The panel mark is Breeze's call-outgoing. The old one was a bell, sitting two inches from Plasma's notification bell in the same bar and meaning something entirely different. Two things it will not do: claim a call was displayed, and let a teacher type the room. The machine already knows which room it is. Sixty tests cover the contract without a broker. The live path was proven against the real coordinator running in the lab, on a loopback broker, with ProPresenter mocked and the church's networks rejected from inside the container first. Co-Authored-By: Claude Opus 5 <[email protected]>
165 lines
6.5 KiB
Bash
Executable File
165 lines
6.5 KiB
Bash
Executable File
#!/bin/bash
|
||
set -uo pipefail
|
||
|
||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
|
||
PYTHONPATH="$ROOT/lib" python3 - <<'PY'
|
||
import json
|
||
|
||
from classroom_parent_call import (
|
||
ERROR, EXPIRED, MODE_APPROVAL, MODE_AUTO, MODE_OFF, REJECTED,
|
||
SENT_TO_SANCTUARY, SUBMITTED, UNAVAILABLE, WAITING_APPROVAL, WORDING,
|
||
Topics, availability, clean_child, new_request_id, read_coordinator,
|
||
read_mode, read_status, request_payload,
|
||
)
|
||
|
||
passed = failed = 0
|
||
|
||
|
||
def check(label, condition, detail=""):
|
||
global passed, failed
|
||
if condition:
|
||
print(f" ok {label}")
|
||
passed += 1
|
||
else:
|
||
print(f" FAIL {label}")
|
||
if detail:
|
||
print(f" {detail}")
|
||
failed += 1
|
||
|
||
|
||
print("1. the coordinator's topics, exactly")
|
||
t = Topics()
|
||
check("root is the church's, with no product name",
|
||
t.root == "classroom/parent-call")
|
||
check("mode", t.mode == "classroom/parent-call/mode")
|
||
check("coordinator", t.coordinator == "classroom/parent-call/coordinator")
|
||
check("request", t.request == "classroom/parent-call/request")
|
||
check("status is per request",
|
||
t.status("nursery-8f21c3") == "classroom/parent-call/status/nursery-8f21c3")
|
||
check("a different root carries through",
|
||
Topics("test/pc").request == "test/pc/request")
|
||
check("a trailing slash does not double up",
|
||
Topics("test/pc/").mode == "test/pc/mode")
|
||
|
||
print()
|
||
print("2. a request is four fields and nothing else")
|
||
body = request_payload("nursery-8f21c3", "nursery", "Emma", now=1780000000)
|
||
check("exactly the four the coordinator reads",
|
||
set(body) == {"id", "room", "child", "created_at"}, sorted(body))
|
||
check("created_at is a number", isinstance(body["created_at"], int))
|
||
check("it round-trips as json", json.loads(json.dumps(body)) == body)
|
||
|
||
print()
|
||
print("3. ids the coordinator will accept")
|
||
check("room-scoped", new_request_id("nursery", "8f21c3") == "nursery-8f21c3")
|
||
check("random by default, and different each time",
|
||
new_request_id("k2") != new_request_id("k2"))
|
||
check("no topic syntax can get into one",
|
||
"/" not in new_request_id("older") and "#" not in new_request_id("older"))
|
||
check("two rooms cannot collide on a fixed part",
|
||
new_request_id("k2", "aaa") != new_request_id("older", "aaa"))
|
||
|
||
print()
|
||
print("4. the name the teacher typed is the name that is sent")
|
||
check("ordinary name", clean_child("Emma") == "Emma")
|
||
check("outer space is trimmed", clean_child(" Emma ") == "Emma")
|
||
check("inner runs collapse", clean_child("Emma Grace") == "Emma Grace")
|
||
check("a newline becomes a space, not a weld",
|
||
clean_child("Emma\nGrace") == "Emma Grace")
|
||
check("a zero width joiner cannot hide in it",
|
||
clean_child("Emma") == "Em ma")
|
||
check("empty is not a name", clean_child(" ") is None)
|
||
check("nothing is not a name", clean_child(None) is None)
|
||
check("longer than the coordinator accepts is refused here first",
|
||
clean_child("E" * 41) is None)
|
||
check("exactly the limit is fine", clean_child("E" * 40) == "E" * 40)
|
||
check("accents normalise to one form",
|
||
clean_child("José") == "José")
|
||
|
||
print()
|
||
print("5. reading the mode")
|
||
for raw, expected in [(b"auto", MODE_AUTO), ("approval", MODE_APPROVAL),
|
||
(" off\n", MODE_OFF), ("AUTO", MODE_AUTO)]:
|
||
check(f"{raw!r} -> {expected}", read_mode(raw) == expected)
|
||
check("anything else is not a mode", read_mode("banana") is None)
|
||
check("an empty retained mode is not a mode", read_mode("") is None)
|
||
|
||
print()
|
||
print("6. is the sanctuary end up")
|
||
check("online", read_coordinator('{"online": true}') is True)
|
||
check("explicitly offline", read_coordinator('{"online": false}') is False)
|
||
check("a cleared retained topic is not up", read_coordinator("") is False)
|
||
check("junk is not up", read_coordinator("yes please") is False)
|
||
check("a bare true is not the contract", read_coordinator("true") is False)
|
||
|
||
print()
|
||
print("7. statuses, as the coordinator sends them")
|
||
def status(**kw):
|
||
body = {"id": "nursery-8f21c3", "room": "nursery",
|
||
"status": SUBMITTED, "message": "Sending", "updated_at": 1}
|
||
body.update(kw)
|
||
return json.dumps(body)
|
||
|
||
seen = read_status(status(status=WAITING_APPROVAL, message="Waiting for approval"))
|
||
check("waiting for approval is not terminal",
|
||
seen["status"] == WAITING_APPROVAL and not seen["terminal"])
|
||
check("its wording is the coordinator's, verbatim",
|
||
seen["message"] == "Waiting for approval")
|
||
|
||
seen = read_status(status(status=SENT_TO_SANCTUARY, message="Sent to sanctuary"))
|
||
check("sent to sanctuary is terminal and good",
|
||
seen["terminal"] and seen["good"])
|
||
check("and it never says displayed",
|
||
"displayed" not in seen["message"].lower())
|
||
|
||
for bad in (REJECTED, EXPIRED, ERROR, UNAVAILABLE):
|
||
seen = read_status(status(status=bad))
|
||
check(f"{bad} is terminal but not good",
|
||
seen["terminal"] and not seen["good"])
|
||
|
||
check("an unknown status is not shown",
|
||
read_status(status(status="displayed")) is None)
|
||
check("json that is not an object is not a status",
|
||
read_status("[1,2,3]") is None)
|
||
check("junk is not a status", read_status("not json") is None)
|
||
check("a status with no usable message falls back to the coordinator's word",
|
||
read_status(status(message=""))["message"] == WORDING[SUBMITTED])
|
||
check("another room's request is never shown here",
|
||
read_status(status(id="k2-ffffff"), request_id="nursery-8f21c3") is None)
|
||
check("our own request is",
|
||
read_status(status(), request_id="nursery-8f21c3")["status"] == SUBMITTED)
|
||
check("a detail is carried but bounded",
|
||
len(read_status(status(detail="x" * 500))["detail"]) == 200)
|
||
|
||
print()
|
||
print("8. when a teacher may send at all")
|
||
can, why = availability(MODE_AUTO, True)
|
||
check("auto with the coordinator up", can and why is None)
|
||
can, why = availability(MODE_APPROVAL, True)
|
||
check("approval with the coordinator up", can and why is None)
|
||
|
||
can, why = availability(MODE_OFF, True)
|
||
check("off cannot send", not can)
|
||
check("and says so in the coordinator's own words",
|
||
why == WORDING[UNAVAILABLE], why)
|
||
|
||
can, why = availability(MODE_AUTO, False)
|
||
check("the coordinator being down stops it", not can)
|
||
check("and does not pretend a call would arrive",
|
||
"not running" in why, why)
|
||
|
||
can, why = availability(MODE_AUTO, True, broker_reachable=False)
|
||
check("no broker stops it", not can)
|
||
check("and says the network, not the sanctuary",
|
||
"network" in why, why)
|
||
|
||
can, why = availability(None, True)
|
||
check("a broker with no retained mode stops it", not can)
|
||
check("and says it is not set up", "set up" in why, why)
|
||
|
||
print()
|
||
print(f"{passed} passed, {failed} failed")
|
||
raise SystemExit(1 if failed else 0)
|
||
PY
|