Discord drops an empty outbound message instead of sending it (#78815)
* fix(discord): reject empty outbound messages * test(discord): cover empty final reply backfill state Missed-message backfill decides what to replay from discord_messages, so a dropped final reply must be recorded as failed by the new guard the same way the exception path records one — otherwise the reply is both never sent and never retried. Co-authored-by: Jony <619963502@qq.com> * chore: map 619963502@qq.com to zyz619963502zyz for PR #73449 salvage --------- Co-authored-by: Jony <619963502@qq.com>
This commit is contained in:
parent
9712b8f0cc
commit
b3e45a3d46
|
|
@ -0,0 +1,2 @@
|
|||
zyz619963502zyz
|
||||
# PR #73449 salvage → #78815
|
||||
|
|
@ -23,6 +23,7 @@ import subprocess
|
|||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
from collections import defaultdict
|
||||
from contextlib import suppress
|
||||
from typing import Callable, Dict, List, Optional, Any, Tuple
|
||||
|
|
@ -3040,6 +3041,29 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
"""
|
||||
if not self._client:
|
||||
return SendResult(success=False, error="Not connected")
|
||||
if not (content or "").strip():
|
||||
logger.warning(
|
||||
"[%s] Dropped empty message to chat=%s (caller bug). Call site:\n%s",
|
||||
self.name,
|
||||
chat_id,
|
||||
"".join(traceback.format_stack(limit=12)[:-1]),
|
||||
)
|
||||
result = SendResult(
|
||||
success=False,
|
||||
error="Refusing to send empty message",
|
||||
)
|
||||
# Mirror the exception path's recovery bookkeeping. Missed-message
|
||||
# backfill decides what to replay from this table, so a dropped
|
||||
# final reply must be recorded as failed — otherwise the reply is
|
||||
# both never sent and never retried.
|
||||
await asyncio.to_thread(
|
||||
self._record_discord_response,
|
||||
reply_to=reply_to,
|
||||
result=result,
|
||||
content=content,
|
||||
final=bool(metadata and metadata.get("notify")),
|
||||
)
|
||||
return result
|
||||
|
||||
try:
|
||||
# Determine target channel: thread_id in metadata takes precedence.
|
||||
|
|
|
|||
|
|
@ -47,6 +47,41 @@ _ensure_discord_mock()
|
|||
from plugins.platforms.discord.adapter import DiscordAdapter # noqa: E402
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_rejects_whitespace_and_records_failed_final_reply(
|
||||
caplog, monkeypatch, tmp_path
|
||||
):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
monkeypatch.setenv("DISCORD_MISSED_MESSAGE_BACKFILL", "true")
|
||||
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="***"))
|
||||
channel = SimpleNamespace(send=AsyncMock())
|
||||
get_channel = MagicMock(return_value=channel)
|
||||
adapter._client = SimpleNamespace(
|
||||
get_channel=get_channel,
|
||||
fetch_channel=AsyncMock(),
|
||||
)
|
||||
with caplog.at_level("WARNING"):
|
||||
result = await adapter.send(
|
||||
"555",
|
||||
" \n\t ",
|
||||
reply_to="123",
|
||||
metadata={"notify": True},
|
||||
)
|
||||
|
||||
assert result.success is False
|
||||
assert result.error == "Refusing to send empty message"
|
||||
get_channel.assert_not_called()
|
||||
channel.send.assert_not_awaited()
|
||||
row = adapter._with_discord_recovery_db(
|
||||
lambda conn: conn.execute(
|
||||
"SELECT status, replied, outage_response, response_message_id "
|
||||
"FROM discord_messages WHERE message_id='123'"
|
||||
).fetchone()
|
||||
)
|
||||
assert tuple(row) == ("failed", 0, 0, None)
|
||||
assert "Dropped empty message to chat=555" in caplog.text
|
||||
|
||||
|
||||
def _voice_adapter(reference_obj, *, native_result=None, native_error=None):
|
||||
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="***"))
|
||||
ref_msg = SimpleNamespace(id=99, to_reference=MagicMock(return_value=reference_obj))
|
||||
|
|
|
|||
Loading…
Reference in New Issue