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]>
62 lines
2.1 KiB
Python
62 lines
2.1 KiB
Python
#!/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()
|