fix(gateway): mark unconfigured platforms as non-retryable to stop reconnect loop

A platform with a missing dependency or missing credentials can never
succeed on retry, but connect() returned bare False, so the gateway
treated the failure as transient and queued it for background
reconnection — looping forever at the backoff cap. Set
_set_fatal_error(..., retryable=False) for missing-dependency and
missing-credential failures in the Slack, Telegram, and Discord
adapters so the reconnect watcher drops them from the retry queue.

Salvaged from PR #31057 by @dskwe (reapplied onto the plugin-migrated
adapter paths). Fixes #31049.
This commit is contained in:
Yuan Li 2026-07-22 03:56:18 -07:00 committed by Teknium
parent 77beb6a085
commit 54a0f07101
5 changed files with 105 additions and 0 deletions

View File

@ -1037,6 +1037,7 @@ class DiscordAdapter(BasePlatformAdapter):
"""Connect to Discord and start receiving events."""
if not DISCORD_AVAILABLE:
logger.error("[%s] discord.py not installed. Run: pip install discord.py", self.name)
self._set_fatal_error("missing_dependency", "discord.py not installed", retryable=False)
return False
# Load opus codec for voice channel support
@ -1073,6 +1074,7 @@ class DiscordAdapter(BasePlatformAdapter):
if not self.config.token:
logger.error("[%s] No bot token configured", self.name)
self._set_fatal_error("missing_credentials", "No bot token configured", retryable=False)
return False
try:

View File

@ -1115,6 +1115,7 @@ class SlackAdapter(BasePlatformAdapter):
logger.error(
"[Slack] slack-bolt not installed. Run: pip install slack-bolt",
)
self._set_fatal_error("missing_dependency", "slack-bolt not installed", retryable=False)
return False
raw_token = self.config.token

View File

@ -3434,10 +3434,12 @@ class TelegramAdapter(BasePlatformAdapter):
"[%s] python-telegram-bot not installed. Run: pip install python-telegram-bot",
self.name,
)
self._set_fatal_error("missing_dependency", "python-telegram-bot not installed", retryable=False)
return False
if not self.config.token:
logger.error("[%s] No bot token configured", self.name)
self._set_fatal_error("missing_credentials", "No bot token configured", retryable=False)
return False
try:

View File

@ -1076,3 +1076,37 @@ async def test_safe_sync_detects_contexts_drift():
fake_http.edit_global_command.assert_not_awaited()
fake_http.delete_global_command.assert_awaited_once_with(999, 77)
fake_http.upsert_global_command.assert_awaited_once_with(999, desired)
# ============================================================================
# #31049: unconfigured platform skips reconnection (non-retryable fatal error)
# ============================================================================
class TestDiscordUnconfiguredNonRetryable:
"""Verify that missing dependency/token sets a non-retryable fatal error
so the gateway does not queue the platform for background reconnection."""
@pytest.mark.asyncio
async def test_no_discord_lib_sets_non_retryable_fatal(self, monkeypatch):
"""connect() with discord.py unavailable → non-retryable fatal error."""
_ensure_discord_mock()
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="fake"))
# Simulate discord.py not installed
monkeypatch.setattr(discord_platform, "DISCORD_AVAILABLE", False)
result = await adapter.connect()
assert result is False
assert adapter.has_fatal_error is True
assert adapter.fatal_error_retryable is False
assert adapter.fatal_error_code == "missing_dependency"
@pytest.mark.asyncio
async def test_no_bot_token_sets_non_retryable_fatal(self, monkeypatch):
"""connect() with empty token → non-retryable fatal error."""
_ensure_discord_mock()
monkeypatch.setattr(discord_platform, "DISCORD_AVAILABLE", True)
adapter = DiscordAdapter(PlatformConfig(enabled=True, token=""))
result = await adapter.connect()
assert result is False
assert adapter.has_fatal_error is True
assert adapter.fatal_error_retryable is False
assert adapter.fatal_error_code == "missing_credentials"

View File

@ -0,0 +1,66 @@
"""Tests for Telegram connect() non-retryable fatal error on missing credentials.
When Telegram has no bot token or no python-telegram-bot installed, connect()
must set a non-retryable fatal error so the gateway does not queue it for
background reconnection (#31049).
"""
import sys
from unittest.mock import MagicMock
import pytest
from gateway.config import PlatformConfig
def _ensure_telegram_mock():
if "telegram" in sys.modules and hasattr(sys.modules["telegram"], "__file__"):
return
telegram_mod = MagicMock()
telegram_mod.ext.ContextTypes.DEFAULT_TYPE = type(None)
telegram_mod.constants.ParseMode.MARKDOWN_V2 = "MarkdownV2"
telegram_mod.constants.ChatType.GROUP = "group"
telegram_mod.constants.ChatType.SUPERGROUP = "supergroup"
telegram_mod.constants.ChatType.CHANNEL = "channel"
telegram_mod.constants.ChatType.PRIVATE = "private"
telegram_mod.error.NetworkError = type("NetworkError", (OSError,), {})
telegram_mod.error.TimedOut = type("TimedOut", (OSError,), {})
telegram_mod.error.BadRequest = type("BadRequest", (Exception,), {})
for name in ("telegram", "telegram.ext", "telegram.constants", "telegram.request"):
sys.modules.setdefault(name, telegram_mod)
sys.modules.setdefault("telegram.error", telegram_mod.error)
_ensure_telegram_mock()
import plugins.platforms.telegram.adapter as telegram_mod # noqa: E402
from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402
class TestTelegramUnconfiguredNonRetryable:
"""Verify that missing dependency/token sets a non-retryable fatal error."""
@pytest.mark.asyncio
async def test_no_telegram_lib_sets_non_retryable_fatal(self, monkeypatch):
"""connect() with python-telegram-bot unavailable → non-retryable fatal error."""
adapter = TelegramAdapter(PlatformConfig(enabled=True, token="fake"))
monkeypatch.setattr(telegram_mod, "TELEGRAM_AVAILABLE", False)
result = await adapter.connect()
assert result is False
assert adapter.has_fatal_error is True
assert adapter.fatal_error_retryable is False
assert adapter.fatal_error_code == "missing_dependency"
@pytest.mark.asyncio
async def test_no_bot_token_sets_non_retryable_fatal(self, monkeypatch):
"""connect() with empty token → non-retryable fatal error."""
monkeypatch.setattr(telegram_mod, "TELEGRAM_AVAILABLE", True)
adapter = TelegramAdapter(PlatformConfig(enabled=True, token=""))
result = await adapter.connect()
assert result is False
assert adapter.has_fatal_error is True
assert adapter.fatal_error_retryable is False
assert adapter.fatal_error_code == "missing_credentials"