'start' was a plain alias for 'pause' — both branches called the same toggle:
if data == 'start' or data == 'pause':
start_or_pause_bot(controllers)
So a caller retrying a timed-out 'start' PAUSED the bot. On 2026-08-28 a
restart routine sent it three times and the bot sat frozen for 4h50m of an
overnight run, stopping at the next state change because trigger_or_stop blocks
while _pausing is set.
The verification that should have caught it failed too: 'status' returned
controllers.game.is_running, which tracks the game controller and not
Bot._pausing, so a paused bot answered running=True.
Both fixed:
- 'start' is idempotent — starts a stopped bot, resumes a paused one, and is a
no-op on a healthy one. 'pause'/'toggle' remain the toggle.
- 'status' reports "running=X paused=Y".
The handler was extracted from an inline closure into handle_hermes_command()
so this is testable behaviourally rather than by asserting on source text.
Verified by falsification: restoring the original semantics makes the suite
fail with "repeated 'start' paused a healthy bot" and "status hides the pause
state: 'running=True'"; the fix makes all five pass.
Co-Authored-By: Claude Opus 5 <[email protected]>
93 lines
3.0 KiB
Python
93 lines
3.0 KiB
Python
"""The control socket must not be able to pause the bot by accident.
|
|
|
|
2026-08-28: `start` was a plain alias for `pause` — both called the same
|
|
toggle. A restart routine that retried a timed-out `start` sent it three times
|
|
and left the bot PAUSED. It sat frozen for 4h50m of an overnight run.
|
|
|
|
The verification that was supposed to catch that also failed: `status` reported
|
|
`controllers.game.is_running`, which tracks the game controller, not
|
|
Bot._pausing. A paused bot answered running=True.
|
|
"""
|
|
import types
|
|
|
|
import pytest
|
|
|
|
|
|
class _FakeBot:
|
|
def __init__(self, pausing=False):
|
|
self._pausing = pausing
|
|
|
|
|
|
class _FakeGame:
|
|
def __init__(self, running=False, pausing=False):
|
|
self.is_running = running
|
|
self.bot = _FakeBot(pausing) if running else None
|
|
self.toggles = 0
|
|
self.starts = 0
|
|
|
|
|
|
class _FakeControllers:
|
|
def __init__(self, running=False, pausing=False):
|
|
self.game = _FakeGame(running, pausing)
|
|
self.debugger = types.SimpleNamespace(is_running=False, stop=lambda: None, start=lambda: None)
|
|
|
|
|
|
@pytest.fixture
|
|
def patched(monkeypatch):
|
|
import main
|
|
|
|
def fake_toggle(controllers):
|
|
g = controllers.game
|
|
if g.is_running:
|
|
g.toggles += 1
|
|
g.bot._pausing = not g.bot._pausing
|
|
else:
|
|
g.starts += 1
|
|
g.is_running = True
|
|
g.bot = _FakeBot(False)
|
|
|
|
monkeypatch.setattr(main, "start_or_pause_bot", fake_toggle)
|
|
return main
|
|
|
|
|
|
def test_repeated_start_never_pauses_a_running_bot(patched):
|
|
"""THE regression. Three starts must leave the bot running and unpaused."""
|
|
c = _FakeControllers(running=True, pausing=False)
|
|
for _ in range(3):
|
|
patched.handle_hermes_command("start", c)
|
|
assert c.game.is_running is True
|
|
assert c.game.bot._pausing is False, "repeated 'start' paused a healthy bot"
|
|
assert c.game.toggles == 0, "'start' toggled a bot that was already running"
|
|
|
|
|
|
def test_start_starts_a_stopped_bot(patched):
|
|
c = _FakeControllers(running=False)
|
|
patched.handle_hermes_command("start", c)
|
|
assert c.game.is_running is True
|
|
assert c.game.starts == 1
|
|
|
|
|
|
def test_start_resumes_a_paused_bot(patched):
|
|
c = _FakeControllers(running=True, pausing=True)
|
|
patched.handle_hermes_command("start", c)
|
|
assert c.game.bot._pausing is False, "'start' did not resume a paused bot"
|
|
|
|
|
|
def test_pause_still_toggles(patched):
|
|
c = _FakeControllers(running=True, pausing=False)
|
|
patched.handle_hermes_command("pause", c)
|
|
assert c.game.bot._pausing is True
|
|
patched.handle_hermes_command("pause", c)
|
|
assert c.game.bot._pausing is False
|
|
|
|
|
|
def test_status_reports_pause_state(patched):
|
|
"""A paused bot answering running=True is what hid the outage."""
|
|
c = _FakeControllers(running=True, pausing=True)
|
|
reply = patched.handle_hermes_command("status", c)
|
|
assert "paused=True" in reply, f"status hides the pause state: {reply!r}"
|
|
assert "running=True" in reply
|
|
|
|
c2 = _FakeControllers(running=True, pausing=False)
|
|
assert "paused=False" in patched.handle_hermes_command("status", c2)
|