"""Focused state-machine test for the launch re-arm fix (no sockets, no game). Reproduces the operator's wedge in-process: a round runs (launches_sent=2), only SOME pods rejoin, LAUNCH is pressed. Before the fix the press was swallowed with no output; after it, the relay re-arms and serves the launch. """ import io import os import sys REPO = r"C:\git\bt411" sys.path.insert(0, os.path.join(REPO, "tools")) os.chdir(os.path.join(REPO, "content")) import btconsole # noqa: E402 EGG = os.path.join(REPO, "content", "FOGDAY.EGG") fails = [] def check(label, ok, extra=""): print(" %-52s %s %s" % (label, "OK" if ok else "*** FAIL ***", extra)) if not ok: fails.append(label) def new_relay(): r = btconsole.Relay(1500, EGG, "127.0.0.1", 0, manual_launch=True) return r class FakeSock: """Records what the relay would have put on the wire.""" def __init__(self): self.sent = [] def settimeout(self, _t): pass def setblocking(self, _b): pass def sendall(self, data): self.sent.append(data) class FakeConn: """Stands in for a pod's console connection.""" def __init__(self, acked=True): self.egg_sent = True self.acked = acked self.addr = ("10.0.0.9", 5000) self.last_seen = btconsole.time.time() self.sock = FakeSock() print("=== 1. THE REPORTED WEDGE: a round ran, a partial rejoin, LAUNCH pressed ===") r = new_relay() print(" roster seats: %d" % len(r.roster)) # A mission ran to completion. r.eggs_released = True r.launches_sent = 2 r.mission_started_at = btconsole.time.time() - 600 r.stop_sent = True # Only SOME of the last round's pods came back (fewer acked conns than seats). r.console_conns = [FakeConn(acked=True)] assert r._pods_ready() < len(r.roster), "test needs a PARTIAL rejoin" # The operator presses LAUNCH. r.launch_requested = True r._tick_launch() check("launches_sent reset from 2", r.launches_sent == 0, "-> %d" % r.launches_sent) check("round re-opened (eggs_released cleared)", r.eggs_released is False) check("roster restored to the template", len(r.roster) == len(r.orig_roster)) check("launch request still pending (not eaten)", r.launch_requested is True) check("stale stop_sent cleared", r.stop_sent is False) print("\n=== 2. A STALE 'End Mission' MUST NOT KILL THE NEXT MISSION ===") r = new_relay() r.launches_sent = 0 # between rounds: nothing is running r.stop_requested = True # operator pressed End Mission out of habit r._tick_stop() check("stop latch consumed while idle", r.stop_requested is False) # and it must not have sent anything / marked a stop check("no StopMission recorded", r.stop_sent is False) print("\n=== 3. AN UN-ACKABLE SEAT: held, and the operator is told about RE-ARM ===") r = new_relay() r.eggs_released = True # eggs are out ... r.eggs_done_at = None # ... but a seat will never ACK, so this stays None r.launches_sent = 0 r.console_conns = [FakeConn(acked=True)] r.launch_requested = True r._tick_launch() # Deliberately NOT auto-re-armed: during normal staging this state just means # "pods are still ACKing", and re-releasing eggs there would restart the handout # under an impatient operator. The explicit rearm command covers the stuck case. check("round NOT silently restarted during staging", r.eggs_released is True) r._rearm_for_new_round("explicit operator re-arm") check("explicit re-arm DOES re-open the round", r.eggs_released is False) print("\n=== 3b. MID-PAIR MUST NEVER RE-ARM (the storm caught on the rig) ===") r = new_relay() r.console_conns = [FakeConn(acked=True) for _ in r.roster] r.eggs_released = True r.eggs_done_at = btconsole.time.time() - 1000 r.launches_sent = 1 # RunMission #1 has gone out r.launch_at = btconsole.time.time() + 5 # #2 is timed and pending r.launch_requested = True # stays set across the pair r._tick_launch() check("no re-arm between RunMission #1 and #2", r.launches_sent == 1, "-> %d" % r.launches_sent) check("eggs NOT re-released mid-pair", r.eggs_released is True) check("pair timer still intact", r.launch_at is not None) print("\n=== 4. THE HAPPY PATH IS UNCHANGED (full roster, first launch) ===") r = new_relay() r.console_conns = [FakeConn(acked=True) for _ in r.roster] r.eggs_released = True r.eggs_done_at = btconsole.time.time() - 1000 # every pod ACKed long ago r.launches_sent = 0 r.launch_requested = True before_roster = len(r.roster) r._tick_launch() check("launch ARMED or fired", r.launch_at is not None or r.launches_sent > 0) check("roster untouched", len(r.roster) == before_roster) check("did NOT spuriously re-arm (eggs still released)", r.eggs_released is True) check("RunMission actually reached the pods", all(c.sock.sent for c in r.console_conns), "%d pkt(s) to pod 1" % len(r.console_conns[0].sock.sent)) print("\n=== 5. REAPER: only a pad that OWES an ACK is ever reaped ===") # Doctrine (2nd review, 2026-07-26): silence is abnormal ONLY while a response # is owed. An ACKed pad's silence is protocol-normal FOREVER (the ACK is its # only message ever) -- the earlier cut reaped every healthy pad 180s into any # manual-launch staging hold. Half-open ACKed ghosts are TCP keepalive's job. r = new_relay() r.launches_sent = 0 r.eggs_released = True owes = FakeConn(acked=False) # egg sent, never ACKed: dead owes.egg_sent_at = btconsole.time.time() - (btconsole.DEAD_PEER_SECONDS + 30) owes.last_seen = owes.egg_sent_at acked_quiet = FakeConn(acked=True) # healthy: silent since its ACK acked_quiet.last_seen = btconsole.time.time() - 9999 r.console_conns = [owes, acked_quiet] r.game_conns = [] dropped = [] r._drop_console = lambda c: (dropped.append(c), r.console_conns.remove(c)) r._reap_dead_peers() check("the never-ACKed pad was reaped", owes in dropped) check("an ACKed pad is NEVER reaped however silent", acked_quiet not in dropped) print("\n=== 5d. REAPER never touches game conns (loading pods are quiet) ===") r = new_relay() r.launches_sent = 0 r.eggs_released = True class FakeGame: # registered, TCP-only, loading addr = ("10.0.0.7", 700); host_id = 2 last_seen = btconsole.time.time() - 9999 r.console_conns = [] r.game_conns = [FakeGame()] r._drop_game = lambda c, why: fails.append("game conn reaped: " + why) r._reap_dead_peers() check("a silent REGISTERED game conn is never reaped", not any("game conn reaped" in f for f in fails)) print("\n=== 5e. the ACK-debt clock starts at EGG SEND, not connect ===") r = new_relay() r.launches_sent = 0 r.eggs_released = True early = FakeConn(acked=False) # connected long ago (held egg) early.last_seen = btconsole.time.time() - 9999 # ...but the egg JUST went out early.egg_sent_at = btconsole.time.time() - 5 r.console_conns = [early] r.game_conns = [] reaped = [] r._drop_console = lambda c: reaped.append(c) r._reap_dead_peers() check("a pad whose egg just went out is given its full window", not reaped) print("\n=== 5f. RE-ARM refusals: running mission / in-flight pair ===") r = new_relay() r.launches_sent = 2 r.stop_sent = False # mission genuinely RUNNING r.eggs_released = True r.console_conns = [] r._rearm_for_new_round("test") check("re-arm REFUSED while a mission is running", r.launches_sent == 2) r.stop_sent = True # round has ENDED r._rearm_for_new_round("test") check("re-arm allowed once the round has ended", r.launches_sent == 0) print("\n=== 5b. REAPER MUST NOT TOUCH A WAITING POD BETWEEN ROUNDS ===") # The regression this guards: a pod waiting for the next mission is byte-silent # by design (its seat beacon is write-only -- L4NET.CPP "the relay ignores its # silence" -- and its console pad has no egg to ACK). A real night showed 12- # and 5-minute gaps between rounds, so a 180s deadline would have dropped # HEALTHY players and forced their clients to relaunch. r = new_relay() r.launches_sent = 0 r.eggs_released = False # between rounds / pre-release waiting = FakeConn(acked=False) waiting.egg_sent = False waiting.last_seen = btconsole.time.time() - 900 # silent for 15 minutes r.console_conns = [waiting] r.game_conns = [] reaped = [] r._drop_console = lambda c: reaped.append(c) r._reap_dead_peers() check("a 15-min-silent waiting pod is NOT reaped", not reaped) print("\n=== 5c. REAPER SKIPS A PAD THAT WAS NEVER SENT AN EGG ===") r = new_relay() r.launches_sent = 0 r.eggs_released = True nopad = FakeConn(acked=False) nopad.egg_sent = False # nothing was asked of it yet nopad.last_seen = btconsole.time.time() - 900 r.console_conns = [nopad] r.game_conns = [] reaped = [] r._drop_console = lambda c: reaped.append(c) r._reap_dead_peers() check("no egg sent -> not reaped", not reaped) print("\n=== 6. REAPER IS DISARMED WHILE A MISSION RUNS ===") r = new_relay() r.launches_sent = 2 # mission running r.eggs_released = True quiet = FakeConn(acked=True) quiet.last_seen = btconsole.time.time() - 9999 r.console_conns = [quiet] r.game_conns = [] r._drop_console = lambda c: fails.append("reaped mid-mission!") r._reap_dead_peers() check("no reaping during a live mission", "reaped mid-mission!" not in fails) print("\n%s" % ("ALL CHECKS PASSED" if not fails else "FAILURES: %s" % fails)) sys.exit(1 if fails else 0)