"""Owns an isolated room broker/controller for a full simulated run and trace replay.""" import argparse import json import os from pathlib import Path import shutil import socket import subprocess import sys import tempfile import time import urllib.request import uuid ROOT = Path(__file__).resolve().parent BASE = "http://127.0.0.1:18776" PORTS = (14232, 18776, 18777) def api(path, body=None): req = urllib.request.Request(BASE + path, data=None if body is None else json.dumps(body).encode(), headers={"Content-Type": "application/json"}) with urllib.request.urlopen(req, timeout=10) as response: return json.load(response) def action(name, entity="", value=None): state = api("/room/state")["state"] payload = {"command_id":str(uuid.uuid4()),"run_id":state["run_id"], "expected_revision":state["revision"],"expires_at_ms":int(time.time()*1000)+10000, "input":{"action":name,"entity":entity,"value":value}} result = api("/room/command", payload) assert result["accepted"], (name, result["message"]) return result["state"] def golden(): action("prepare", value={"profile":"standard","party":6,"seed":42}) action("start_run") action("briefing_done") for puzzle, token in [("P01",None),("P02A","T01"),("P02B","T02"),("P03","T03"),("P04","T04"),("P05","T05"),("P06","T06")]: action("solve", puzzle) if token: action("deliver", token) for i in range(1,7): cabinet=f"cabinet_{i:02}" action("token", cabinet, {"token":f"T{i:02}","age_ms":0}) action("start", cabinet) state=action("advance", value=30000) for station in state["stations"]: action("win", station["id"], {"attempt":station["attempt"],"progress":station["target"]}) action("boss_start") state=action("boss_win") effect=next(e for e in state["effects"] if e["kind"]=="passage.release") assert not action("ack", effect["id"])["passage_open"] action("passage", value=True) state=action("advance", value=300) for station in state["stations"]: action("bay", station["id"]) action("cell","BAY"+station["cell"][4:],{"cell":station["cell"],"present":False}) action("cell",station["socket"],{"cell":station["cell"],"present":True}) state=action("advance", value=2500) while state["phase"] in ("CALIBRATION_TUTORIAL","CALIBRATION"): wave=state["wave"] for station,target in wave["targets"].items(): action("ready",station,{"challenge":wave["id"],"selection":target,"stable_ms":2000}) state=action("execute", value=wave["id"]) assert state["phase"]=="LAUNCH_ARM" epoch=state["launch_epoch"] for station in state["stations"]: action("arm",station["id"],epoch) for group in range(state["operators"]): action("group_ready",value={"epoch":epoch,"group":group}) action("commit",value=epoch) state=action("advance",value=5000) assert state["lifecycle"]=="WON" and state["launches"]==1 assert all(s["passes"]==2 for s in state["stations"]) return state def main(): parser=argparse.ArgumentParser(description=__doc__) parser.add_argument("--preview",action="store_true",help="After tests, serve GM UI for up to 10 minutes; create qa.stop to stop early") args=parser.parse_args() for port in PORTS: with socket.socket() as sock: sock.bind(("127.0.0.1",port)) nats=shutil.which("nats-server") binary=ROOT/"target/debug"/("glitch-odin-lab.exe" if os.name=="nt" else "glitch-odin-lab") if not nats or not binary.is_file(): raise RuntimeError("Install NATS and build the Rust lab first") children=[] output=ROOT/"evidence" output.mkdir(exist_ok=True) with tempfile.TemporaryFile(mode="w+b") as log: def spawn(argv): child=subprocess.Popen(argv,cwd=ROOT,stdout=log,stderr=log, creationflags=subprocess.CREATE_NO_WINDOW if os.name=="nt" else 0) children.append(child) return child try: spawn([nats,"-c",str(ROOT/"room-nats.conf")]) deadline=time.monotonic()+10 while True: try: with socket.create_connection(("127.0.0.1",14232),timeout=.2): break except OSError: if time.monotonic()>deadline: raise time.sleep(.1) spawn([str(binary),"room"]) deadline=time.monotonic()+20 while True: try: api("/room/state") break except OSError: if time.monotonic()>deadline: raise time.sleep(.1) state=golden() trace=api("/room/trace") trace_path=output/"room-golden-trace.json" trace_path.write_text(json.dumps(trace,indent=2),encoding="utf-8") subprocess.run([str(binary),"replay-room",str(trace_path)],check=True) (output/"room-result.json").write_text(json.dumps({ "status":"passed","transport":"HTTP -> Rust -> NATS -> Rust room reducer", "lifecycle":state["lifecycle"],"launches":state["launches"], "passes":[s["passes"] for s in state["stations"]], "final_hash":trace["final_hash"],"trace_events":len(trace["trace"]) },indent=2),encoding="utf-8") print("PASS: complete seven-cabinet room, puzzles, cells, practices, calibration, launch, and offline trace replay",flush=True) if args.preview: action("reset",value={"confirmed":True}) action("start_run") action("briefing_done") action("solve","P01") action("solve","P02A") action("deliver","T01") action("token","cabinet_01",{"token":"T01","age_ms":0}) action("start","cabinet_01") action("advance",value=10000) spawn([sys.executable,str(ROOT/"room_web.py")]) stop=ROOT/"qa.stop" if stop.exists(): stop.unlink() print("PREVIEW: http://127.0.0.1:18777/?room=1",flush=True) deadline=time.monotonic()+600 while time.monotonic()