"""Start real HTTP services with fake GPIO, then a real headless Godot cabinet.""" import os from pathlib import Path import subprocess import sys import tempfile import threading import time import json import urllib.request import urllib.error from http.server import HTTPServer from test_room import ROOT, server sys.path.insert(0, str(ROOT / "tests/gpio")) from test_bridge import bridge, FakePin OP = "test-operator-token-1234" CAB = "test-cabinet-token-5678" def post(path, data=None, token=OP): request = urllib.request.Request("http://127.0.0.1:8766" + path, data=json.dumps(data or {}).encode(), headers={"Authorization": "Bearer " + token, "Content-Type": "application/json"}) with urllib.request.urlopen(request, timeout=3) as response: return json.load(response) def wait_for(predicate): deadline = time.monotonic() + 10 while time.monotonic() < deadline: state = post("/operator/state") if predicate(state): return state time.sleep(.1) raise AssertionError("Timed out waiting for room state: " + str(state)) with tempfile.TemporaryDirectory() as temp: room = server.Fleet(Path(temp) / "room.sqlite3", json.loads((ROOT / "tools/room_controller/catalog.json").read_text())) gpio = bridge.Outputs(FakePin, "cabinet_01_gpio", [17], ledger=Path(temp) / "gpio.sqlite3") services = [HTTPServer(("127.0.0.1", 8766), server.handler_for(room, OP, {"cabinet_01": CAB, "cabinet_02": CAB + "-two"})), HTTPServer(("127.0.0.1", 8765), bridge.handler_for(gpio))] threads = [threading.Thread(target=s.serve_forever, daemon=True) for s in services] for thread in threads: thread.start() process = None second = None try: try: post("/operator/state", token=CAB) raise AssertionError("Cabinet token accessed operator API") except urllib.error.HTTPError as exc: assert exc.code == 401 env = dict(os.environ, ARCADE_ROOM_URL="http://127.0.0.1:8766", ARCADE_CABINET_TOKEN=CAB) process = subprocess.Popen([sys.argv[1], "--headless", "--path", str(ROOT), "--log-file", str(ROOT / ".godot/managed_integration.log"), "--script", "tests/room_network/managed_agent.gd", "--", "--gpio=real"], env=env) second = subprocess.Popen([sys.argv[1], "--headless", "--path", str(ROOT), "--log-file", str(ROOT / ".godot/managed_second.log"), "--script", "tests/room_network/managed_agent.gd", "--", "--cabinet=cabinet_02", "--test-no-reward"], env=dict(env, ARCADE_CABINET_TOKEN=CAB + "-two")) wait_for(lambda s: s["online"] and s["cabinets"]["cabinet_02"]["online"]) state = post("/operator/command", {"action": "prepare_scenario", "scenario": "proof_of_concept"}) post("/operator/command", {"action": "prepare", "cabinet": "cabinet_02", "game": "galaga"}) assert state["phase"] == "preparing" wait_for(lambda s: s["phase"] == "ready" and s["cabinets"]["cabinet_02"]["phase"] == "ready") post("/operator/command", {"action": "start"}) post("/operator/command", {"action": "start", "cabinet": "cabinet_02"}) final = wait_for(lambda s: s["reward"].startswith("GPIO accepted")) wait_for(lambda s: s["cabinets"]["cabinet_02"]["phase"] == "won") assert gpio.pins[17]["device"].activations == 1 assert sum(c["type"] == "reward" for c in final["commands"]) == 1 assert process.wait(timeout=8) == 0 assert second.wait(timeout=8) == 0 assert not any(c["type"] == "reward" for c in post("/operator/state")["cabinets"]["cabinet_02"]["commands"]) print("PASS: two real Godot cabinets -> Pac-Man scenario + Galaga manual -> ready/start/win -> exactly one mapped GPIO acceptance") finally: if process and process.poll() is None: process.terminate() process.wait(timeout=5) if second and second.poll() is None: second.terminate() second.wait(timeout=5) for service in services: service.shutdown() service.server_close() for thread in threads: thread.join() gpio.close() for cabinet in room.rooms.values(): cabinet.db.close()