"""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=== POST-TRIM: explicit tag beats the positional fallback ===") # After a LAUNCH trim the relay remaps seat ids: host 2 can be roster position 3. # The relay now prints tag='...' on REGISTERED/dropped; the monitor must use it. if len(tags) > 1: # relay says host 2, but the tag names row 1 -- the trimmed truth feed("[relay] PLAYER 2 SEATED (host 2) tag='%s' callsign='TRIMMED' mech='thor'" % tags[1]) feed("[relay] game[5.5.5.5:5 host=2] REGISTERED (1/1) tag='%s'" % tags[1]) check("explicit tag lights the RIGHT row", light(1) == btoperator.ST_REGISTERED, str(light(1))) check("row 0 not falsely lit", light(0) != btoperator.ST_REGISTERED, str(light(0))) feed("[relay] game[5.5.5.5:5 host=2] dropped: closed tag='%s'" % tags[1]) check("explicit tag clears the RIGHT row", light(1) == btoperator.ST_IDLE, str(light(1))) check("the departed name cleared on the right row too", callsign(1) != "TRIMMED", callsign(1)) # old-format line (no tag=): the positional fallback must still work feed("[relay] PLAYER 1 SEATED (host 2) tag='%s' callsign='OLDFMT' mech='lok2'" % tags[0]) feed("[relay] game[5.5.5.5:5 host=2] REGISTERED (1/1)") check("legacy line (no tag) still resolves positionally", light(0) == btoperator.ST_REGISTERED, str(light(0))) feed("[relay] game[5.5.5.5:5 host=2] dropped: closed") print("\n=== stash survives NOTHING it should not: stopped-while-occupied ===") # A session stopped while a seat is occupied never pops the stash. The next # session must not restore last session's snapshot -- _start_session clears it. feed("[relay] PLAYER 1 SEATED (host 2) tag='%s' callsign='GHOST' mech='lok2'" % tags[0]) check("occupied at 'session stop'", callsign(0) == "GHOST", callsign(0)) # simulate the parts of _start_session that matter (no relay spawn) win._seat_defaults.clear() win.monitor = btoperator.SessionMonitor(True, tags) win.table.item(0, 1).setText("FRESH_CFG") # operator reconfigures feed("[relay] PLAYER 1 SEATED (host 2) tag='%s' callsign='NEWSESSION' mech='thor'" % tags[0]) feed("[relay] game[8.8.8.8:8 host=2] dropped: closed") check("post-restart empty seat shows the NEW config, not GHOST", callsign(0) == "FRESH_CFG", callsign(0)) print("\n=== stash is TAG-keyed: an egg reload makes old keys inert ===") win._seat_defaults["10.99.9.9:9999"] = ("STALE", "lok2") # a tag not in this roster feed("[relay] PLAYER 1 SEATED (host 2) tag='%s' callsign='ABC' mech='lok2'" % tags[0]) feed("[relay] game[8.8.8.8:8 host=2] dropped: closed") check("a stale foreign-tag stash never touches a live row", callsign(0) == "FRESH_CFG", callsign(0)) print("\n%s" % ("ALL CHECKS PASSED" if not fails else "FAILURES: %s" % fails)) sys.exit(1 if fails else 0)