diff --git a/gateway/platforms/discord.py b/gateway/platforms/discord.py index 332d83f52..eaf457fc0 100644 --- a/gateway/platforms/discord.py +++ b/gateway/platforms/discord.py @@ -605,10 +605,30 @@ class DiscordAdapter(BasePlatformAdapter): logger.debug("Could not fetch reply-to message: %s", e) for i, chunk in enumerate(chunks): - msg = await channel.send( - content=chunk, - reference=reference if i == 0 else None, - ) + chunk_reference = reference if i == 0 else None + try: + msg = await channel.send( + content=chunk, + reference=chunk_reference, + ) + except Exception as e: + err_text = str(e) + if ( + chunk_reference is not None + and "error code: 50035" in err_text + and "Cannot reply to a system message" in err_text + ): + logger.warning( + "[%s] Reply target %s is a Discord system message; retrying send without reply reference", + self.name, + reply_to, + ) + msg = await channel.send( + content=chunk, + reference=None, + ) + else: + raise message_ids.append(str(msg.id)) return SendResult( diff --git a/tests/gateway/test_discord_send.py b/tests/gateway/test_discord_send.py new file mode 100644 index 000000000..f8cb5dead --- /dev/null +++ b/tests/gateway/test_discord_send.py @@ -0,0 +1,76 @@ +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock +import sys + +import pytest + +from gateway.config import PlatformConfig + + +def _ensure_discord_mock(): + if "discord" in sys.modules and hasattr(sys.modules["discord"], "__file__"): + return + + discord_mod = MagicMock() + discord_mod.Intents.default.return_value = MagicMock() + discord_mod.Client = MagicMock + discord_mod.File = MagicMock + discord_mod.DMChannel = type("DMChannel", (), {}) + discord_mod.Thread = type("Thread", (), {}) + discord_mod.ForumChannel = type("ForumChannel", (), {}) + discord_mod.ui = SimpleNamespace(View=object, button=lambda *a, **k: (lambda fn: fn), Button=object) + discord_mod.ButtonStyle = SimpleNamespace(success=1, primary=2, danger=3, green=1, blurple=2, red=3) + discord_mod.Color = SimpleNamespace(orange=lambda: 1, green=lambda: 2, blue=lambda: 3, red=lambda: 4) + discord_mod.Interaction = object + discord_mod.Embed = MagicMock + discord_mod.app_commands = SimpleNamespace(describe=lambda **kwargs: (lambda fn: fn)) + + ext_mod = MagicMock() + commands_mod = MagicMock() + commands_mod.Bot = MagicMock + ext_mod.commands = commands_mod + + sys.modules.setdefault("discord", discord_mod) + sys.modules.setdefault("discord.ext", ext_mod) + sys.modules.setdefault("discord.ext.commands", commands_mod) + + +_ensure_discord_mock() + +from gateway.platforms.discord import DiscordAdapter # noqa: E402 + + +@pytest.mark.asyncio +async def test_send_retries_without_reference_when_reply_target_is_system_message(): + adapter = DiscordAdapter(PlatformConfig(enabled=True, token="***")) + + ref_msg = SimpleNamespace(id=99) + sent_msg = SimpleNamespace(id=1234) + send_calls = [] + + async def fake_send(*, content, reference=None): + send_calls.append({"content": content, "reference": reference}) + if len(send_calls) == 1: + raise RuntimeError( + "400 Bad Request (error code: 50035): Invalid Form Body\n" + "In message_reference: Cannot reply to a system message" + ) + return sent_msg + + channel = SimpleNamespace( + fetch_message=AsyncMock(return_value=ref_msg), + send=AsyncMock(side_effect=fake_send), + ) + adapter._client = SimpleNamespace( + get_channel=lambda _chat_id: channel, + fetch_channel=AsyncMock(), + ) + + result = await adapter.send("555", "hello", reply_to="99") + + assert result.success is True + assert result.message_id == "1234" + assert channel.fetch_message.await_count == 1 + assert channel.send.await_count == 2 + assert send_calls[0]["reference"] is ref_msg + assert send_calls[1]["reference"] is None