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:
parent
77beb6a085
commit
54a0f07101
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
Loading…
Reference in New Issue