"""Roster-display test: a departed player must not keep their seat (2026-07-26). Operator report: "if a player disconnects from the console, it leaves their name up in the roster ... it should turn their seat back to empty." Drives the REAL Qt widget offscreen and feeds it the REAL relay log lines, then asserts what the roster table shows. No relay, no game, no visible window. """ import os import sys os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") REPO = r"C:\git\bt411" sys.path.insert(0, os.path.join(REPO, "tools")) fails = [] def check(label, ok, extra=""): print(" %-58s %s %s" % (label, "OK" if ok else "*** FAIL ***", extra)) if not ok: fails.append(label) from PySide6.QtWidgets import QApplication # noqa: E402 import btoperator # noqa: E402 app = QApplication.instance() or QApplication([]) win = btoperator.Operator() # Relay mode with a fresh monitor over the roster the UI actually built. tags = [] for r in range(win.table.rowCount()): tags.append(win.table.item(r, 0).text().strip()) win.monitor = btoperator.SessionMonitor(True, tags) print(" roster rows: %d first tag: %s" % (len(tags), tags[0] if tags else "-")) def callsign(row): it = win.table.item(row, 1) return it.text() if it is not None else None def light(row): return win.monitor.state.get(tags[row]) def feed(line): win.monitor.feed(line) win._refresh_pod_status() configured = callsign(0) print(" row 0 configured callsign: %r" % configured) print("\n=== a walk-up player takes seat 1 ===") feed("[relay] PLAYER 1 SEATED (host 2) tag='%s' callsign='SAURON' mech='lok2'" % tags[0]) check("roster shows the player's callsign", callsign(0) == "SAURON", callsign(0)) check("pilot light is 'seated'", light(0) == btoperator.ST_SEATED, str(light(0))) print("\n=== they register, then DISCONNECT (the reported case) ===") feed("[relay] game[1.2.3.4:5555 host=2] REGISTERED (1/2)") check("light is 'registered'", light(0) == btoperator.ST_REGISTERED, str(light(0))) feed("[relay] game[1.2.3.4:5555 host=2] dropped: closed") check("pilot light returns to 'waiting'", light(0) == btoperator.ST_IDLE, str(light(0))) check("SEAT NO LONGER SHOWS THE DEPARTED NAME", callsign(0) != "SAURON", callsign(0)) check("seat restored to its configured pilot", callsign(0) == configured, callsign(0)) print("\n=== the seat is reusable: a DIFFERENT player takes it ===") feed("[relay] PLAYER 1 SEATED (host 2) tag='%s' callsign='NEWGUY' mech='thor'" % tags[0]) check("the new player's callsign shows", callsign(0) == "NEWGUY", callsign(0)) feed("[relay] game[9.9.9.9:1 host=2] dropped: closed") check("and it clears again for them too", callsign(0) == configured, callsign(0)) print("\n=== the other clearing path: 'PLAYER n LEFT' (never claimed) ===") feed("[relay] PLAYER 1 SEATED (host 2) tag='%s' callsign='WALKUP' mech='owens'" % tags[0]) check("occupied again", callsign(0) == "WALKUP", callsign(0)) feed("[relay] PLAYER 1 LEFT (host 2) tag='%s' (0 seated)" % tags[0]) check("LEFT also clears the seat", callsign(0) == configured, callsign(0)) check("light is 'waiting'", light(0) == btoperator.ST_IDLE, str(light(0))) print("\n=== an operator edit made while the seat is EMPTY must survive ===") win.table.item(0, 1).setText("MY_DEFAULT") feed("[relay] PLAYER 1 SEATED (host 2) tag='%s' callsign='VISITOR' mech='lok2'" % tags[0]) check("visitor's name shows while seated", callsign(0) == "VISITOR", callsign(0)) feed("[relay] game[7.7.7.7:7 host=2] dropped: closed") check("reverts to the operator's NEW edit, not the old one", callsign(0) == "MY_DEFAULT", callsign(0)) print("\n=== an untouched seat is never rewritten ===") if len(tags) > 1: before = callsign(1) feed("[relay] PLAYER 1 SEATED (host 2) tag='%s' callsign='X' mech='lok2'" % tags[0]) check("row 1 untouched by row 0 traffic", callsign(1) == before, "%r vs %r" % (callsign(1), before)) print("\n%s" % ("ALL CHECKS PASSED" if not fails else "FAILURES: %s" % fails)) sys.exit(1 if fails else 0)