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]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
380f031092
commit
7790003c03
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""A stand-in for the church's ProPresenter, so no test can reach it.
|
||||
|
||||
It serves exactly what the coordinator's adapter checks for: a message
|
||||
called "Parent Caller" with tokens Name and Room, a 204 on trigger, and a
|
||||
204 on clear. Every trigger is appended to a log the test reads, which is
|
||||
how "sent to sanctuary" is verified without a sanctuary.
|
||||
"""
|
||||
import json, sys
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
UUID = "11111111-2222-3333-4444-555555555555"
|
||||
LOG = "/tmp/propresenter-mock.log"
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
def _json(self, body, code=200):
|
||||
payload = json.dumps(body).encode()
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/version":
|
||||
return self._json({"name": "mock", "api_version": "v1"})
|
||||
if self.path == "/v1/messages":
|
||||
return self._json([{
|
||||
"id": {"name": "Parent Caller", "uuid": UUID},
|
||||
"tokens": [{"name": "Name"}, {"name": "Room"}],
|
||||
}])
|
||||
if self.path.endswith("/clear"):
|
||||
with open(LOG, "a") as fh:
|
||||
fh.write("CLEAR\n")
|
||||
self.send_response(204)
|
||||
self.end_headers()
|
||||
return
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def do_POST(self):
|
||||
length = int(self.headers.get("Content-Length") or 0)
|
||||
body = self.rfile.read(length).decode()
|
||||
if self.path.endswith("/trigger"):
|
||||
with open(LOG, "a") as fh:
|
||||
fh.write("TRIGGER " + body + "\n")
|
||||
self.send_response(204)
|
||||
self.end_headers()
|
||||
return
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
open(LOG, "w").close()
|
||||
HTTPServer(("127.0.0.1", int(sys.argv[1] if len(sys.argv) > 1 else 1025)),
|
||||
Handler).serve_forever()
|
||||
Reference in New Issue
Block a user