Files
room_glitch/labs/glitch_odin/room_smoke.py
T

155 lines
6.7 KiB
Python

"""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()<deadline and not stop.exists():
time.sleep(.2)
if stop.exists(): stop.unlink()
finally:
for p in reversed(children):
if p.poll() is None: p.terminate()
for p in children:
try: p.wait(timeout=5)
except subprocess.TimeoutExpired:
p.kill();p.wait()
log.seek(0)
text=log.read().decode("utf-8",errors="replace")
(output/"room-runtime.log").write_text(text,encoding="utf-8")
if __name__=="__main__":
main()