Files
Classroom_Desktop/tests/test-classroom-parent-call.sh
T
ekklesiaandClaude Opus 5 7790003c03 feat: let a teacher ask the sanctuary to call a child's parent
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]>
2026-09-10 10:37:06 -05:00

165 lines
6.5 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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("Em‍ma") == "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