Files
BT411/scratchpad/operator_e2e.py
T
arcattackandClaude Fable 5 5df5c954ae Operator: manual launch gate + restart-session flow
The operator now holds the lobby: with 'Manual launch' checked (default),
the relay stops at 'all pods have the egg; WAITING FOR OPERATOR LAUNCH'
instead of auto-firing on a 20s timer -- the app shows 'ALL PODS READY --
press LAUNCH MISSION' with the roster lights, and the big green button
fires the mission when the operator is ready (everyone seated, voice chat
confirmed).  A 'Restart session' button cycles the same mission/roster for
the next round (players just re-run their join script).

- btconsole.py --relay --manual-launch: launch armed by the line 'launch'
  on stdin (a daemon reader thread; the app's Launch button writes it via
  QProcess).  The settle window is still honoured relative to egg delivery
  (max(now, eggs_done + 20s)) -- firing RunMission before the pods reach
  WaitingForLaunch Fail()s them, the same hazard the auto timer guards.
  Auto mode (no flag) unchanged -- CLI/test recipes unaffected.
- btoperator.py: Manual-launch checkbox (Network box, default on), LAUNCH
  MISSION button (enabled by the relay's READY line, disabled after fire),
  Restart session button, status headline shows the ready state.
- operator_e2e.py: now exercises the gate -- waits for READY, asserts the
  button enabled, presses it, verifies the operator-gated LAUNCH.

Verified: scripted e2e PASS -- registered -> READY (held) -> operator
launch -> settle honoured -> both pods LAUNCHED (operator-gated=True).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:32:54 -05:00

89 lines
2.9 KiB
Python

#!/usr/bin/env python
"""operator_e2e.py -- scripted end-to-end test of the operator console.
Drives the real Operator window through its own methods on a timer:
start a RELAY session -> launch 2 local btl4 instances -> wait for the
mission LAUNCH to be observed by the session monitor -> report + shut down.
Exercises the exact QProcess paths the buttons use (no synthetic clicks).
Exit 0 = launched; 1 = timed out.
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "tools"))
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication, QMessageBox
import btoperator
# never block on dialogs in scripted mode
QMessageBox.warning = staticmethod(lambda *a, **k: print("[e2e] WARN dialog:", a[2] if len(a) > 2 else a))
QMessageBox.information = staticmethod(lambda *a, **k: None)
TIMEOUT_S = 120
result = {"ok": False}
def main():
app = QApplication(sys.argv)
btoperator.dark_palette(app)
win = btoperator.Operator()
win.show()
def step_start():
print("[e2e] starting relay session")
win._start_session()
assert win.console_proc is not None, "session did not start"
QTimer.singleShot(3000, step_launch)
def step_launch():
print("[e2e] launching local instances")
win._launch_local()
assert len(win.game_procs) == 2, \
"expected 2 local instances, got %d" % len(win.game_procs)
poll.start(2000)
ticks = {"n": 0}
pressed = {"launch": False}
def step_poll():
ticks["n"] += 1
m = win.monitor
states = dict(m.state) if m else {}
print("[e2e] t=%ds states=%s ready=%s launched=%s" %
(ticks["n"] * 2, states, m.ready if m else "?",
m.launched if m else "?"))
# MANUAL GATE: when the relay reports ready, press the Launch button.
if m and m.ready and not m.launched and not pressed["launch"]:
assert win.launch_btn.isEnabled(), "Launch button should be enabled"
print("[e2e] relay READY -> pressing LAUNCH MISSION")
win._launch_mission()
pressed["launch"] = True
if m and m.launched:
result["ok"] = True
status = win.pod_status.text().encode("ascii", "replace").decode()
print("[e2e] LAUNCH observed (operator-gated=%s) -- pod status: %s"
% (pressed["launch"], status))
finish()
elif ticks["n"] * 2 >= TIMEOUT_S:
print("[e2e] TIMEOUT")
finish()
def finish():
poll.stop()
win._stop_session()
QTimer.singleShot(500, app.quit)
poll = QTimer()
poll.timeout.connect(step_poll)
QTimer.singleShot(1000, step_start)
app.exec()
print("[e2e] %s" % ("PASS" if result["ok"] else "FAIL"))
return 0 if result["ok"] else 1
if __name__ == "__main__":
sys.exit(main())