Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions plugins/platforms/discord/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
async def _wait_for_ready_or_bot_exit(
ready_event: asyncio.Event,
bot_task: asyncio.Task,
timeout: float,
timeout: Optional[float],
) -> None:
"""Wait until Discord is ready, or surface early bot startup failure.

Expand Down Expand Up @@ -208,6 +208,20 @@ def _b(name: str, default: bool) -> bool:
)


def _discord_ready_timeout_seconds() -> float:
"""Return the Discord ready wait timeout during gateway startup."""
raw = os.getenv("HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT", "").strip()
if raw:
try:
return max(0.0, float(raw))
except ValueError:
logger.warning(
"Ignoring invalid HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT=%r",
raw,
)
return 30.0


class VoiceReceiver:
"""Captures and decodes voice audio from a Discord voice channel.

Expand Down Expand Up @@ -1005,9 +1019,14 @@ async def on_voice_state_update(member, before, after):
self._bot_task = asyncio.create_task(self._client.start(self.config.token))
self._bot_task.add_done_callback(self._handle_bot_task_done)

ready_timeout = _discord_ready_timeout_seconds()
# Wait for ready, but fail fast if discord.py's background startup
# task dies first (for example on SOCKS/proxy connect errors).
await _wait_for_ready_or_bot_exit(self._ready_event, self._bot_task, timeout=30)
await _wait_for_ready_or_bot_exit(
self._ready_event,
self._bot_task,
timeout=None if ready_timeout <= 0 else ready_timeout,
)

self._running = True
return True
Expand Down
37 changes: 36 additions & 1 deletion tests/gateway/test_discord_connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,6 @@ async def fake_wait_for_ready(ready_event, bot_task, timeout):
"leaving it alive creates a zombie Discord client that produces duplicate threads"
)


@pytest.mark.asyncio
async def test_disconnect_cancels_running_bot_task(monkeypatch):
"""Regression: disconnect() must cancel _bot_task even when connect() timed out.
Expand Down Expand Up @@ -363,6 +362,42 @@ async def _forever():
assert zombie_task.cancelled(), "disconnect() must cancel the zombie bot task"


@pytest.mark.asyncio
async def test_connect_ready_wait_uses_gateway_platform_connect_timeout(monkeypatch):
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="test-token"))

monkeypatch.setenv("HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT", "90")
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)
monkeypatch.setattr(
discord_platform.commands,
"Bot",
lambda **kwargs: FakeBot(
intents=kwargs["intents"],
proxy=kwargs.get("proxy"),
allowed_mentions=kwargs.get("allowed_mentions"),
),
)

seen_timeouts = []

async def fake_wait_for_ready(ready_event, bot_task, timeout):
seen_timeouts.append(timeout)
raise asyncio.TimeoutError()

monkeypatch.setattr(
discord_platform, "_wait_for_ready_or_bot_exit", fake_wait_for_ready
)

ok = await adapter.connect()

assert ok is False
assert seen_timeouts == [90.0]


@pytest.mark.asyncio
async def test_connect_does_not_wait_for_slash_sync(monkeypatch):
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="test-token"))
Expand Down
Loading