test(discord): regression coverage for zombie-websocket guard in connect()

Covers PR #18224 fix for issue #18187 — when DiscordAdapter.connect() is
called a second time without an intervening disconnect(), the previous
commands.Bot must be closed before a new one is created. Otherwise both
websockets stay connected to Discord's gateway and both fire on_message,
producing double responses with different wording.
This commit is contained in:
teknium1 2026-05-02 02:03:40 -07:00 committed by Teknium
parent 292d2fb42f
commit e363ced3c3

View File

@ -172,6 +172,69 @@ async def test_connect_only_requests_members_intent_when_needed(monkeypatch, all
await adapter.disconnect() await adapter.disconnect()
@pytest.mark.asyncio
async def test_reconnect_closes_previous_client_to_prevent_zombie_websocket(monkeypatch):
"""Regression for #18187: calling connect() twice without disconnect() in
between (e.g. during an in-process reconnect attempt) must close the old
commands.Bot before creating a new one. Without this guard, two websockets
stay alive and both fire on_message, producing double responses with
different wording.
"""
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="test-token"))
monkeypatch.setattr("gateway.status.acquire_scoped_lock", lambda scope, identity, metadata=None: (True, None))
monkeypatch.setattr("gateway.status.release_scoped_lock", lambda scope, identity: None)
intents = SimpleNamespace(
message_content=False, dm_messages=False, guild_messages=False,
members=False, voice_states=False,
)
monkeypatch.setattr(discord_platform.Intents, "default", lambda: intents)
class TrackedBot(FakeBot):
"""FakeBot that records close() calls and reports open/closed state."""
_closed = False
def is_closed(self):
return self._closed
async def close(self):
self._closed = True
created: list[TrackedBot] = []
def fake_bot_factory(*, command_prefix, intents, proxy=None, allowed_mentions=None, **_):
bot = TrackedBot(intents=intents, allowed_mentions=allowed_mentions)
created.append(bot)
return bot
monkeypatch.setattr(discord_platform.commands, "Bot", fake_bot_factory)
monkeypatch.setattr(adapter, "_resolve_allowed_usernames", AsyncMock())
# First connect — fresh adapter, no prior client.
assert await adapter.connect() is True
assert len(created) == 1
first_bot = created[0]
assert first_bot._closed is False, "first bot should still be open after connect()"
# Second connect WITHOUT disconnect — simulates an in-process reconnect.
# Without the fix, first_bot would remain open (zombie), and both would
# receive every Discord event, causing double responses.
assert await adapter.connect() is True
assert len(created) == 2
second_bot = created[1]
# The first bot must be closed before the second is assigned.
assert first_bot._closed is True, (
"First Discord client must be closed on re-entry of connect() to prevent "
"zombie websocket (#18187)"
)
assert second_bot._closed is False, "second bot should still be open"
assert adapter._client is second_bot
await adapter.disconnect()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_connect_releases_token_lock_on_timeout(monkeypatch): async def test_connect_releases_token_lock_on_timeout(monkeypatch):
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="test-token")) adapter = DiscordAdapter(PlatformConfig(enabled=True, token="test-token"))