mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
fix(discord): split oversized final edits, truncate mid-stream previews (#27881)
DiscordAdapter.edit_message clipped any formatted payload over the 2,000-char cap to [:1997]+"..." and returned success=True, so the stream consumer believed the full reply landed and stopped — the user lost everything past the boundary and perceived the agent as quitting mid-task. edit_message is now overflow-aware, mirroring Telegram's proven contract: - finalize=True: split-and-deliver via _edit_overflow_split — edit chunk 1 in place, send chunks 2..N as reply-threaded continuations, return the last visible id in message_id plus continuation_message_ids so the stream consumer keeps editing the most recent chunk and can clean them all up. - finalize=False (mid-stream): truncate a one-message preview in place, never split. A mid-stream split moves the edit target to a continuation and the next accumulated-token tick re-splits, looping forever (the Telegram #48648 lesson the original port predated). - Reactive 50035 '2000 or fewer in length' on edit runs the same branch logic. - Partial continuation failure still reports success with a partial_overflow raw_response so the consumer retries the tail instead of marking a clipped reply complete. Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com> Co-authored-by: AhmetArif0 <147827411+AhmetArif0@users.noreply.github.com>
This commit is contained in:
parent
ea9f8bd162
commit
af5cea04ab
2 changed files with 503 additions and 3 deletions
|
|
@ -2100,7 +2100,19 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
*,
|
||||
finalize: bool = False,
|
||||
) -> SendResult:
|
||||
"""Edit a previously sent Discord message."""
|
||||
"""Edit a previously sent Discord message.
|
||||
|
||||
Discord caps single-message text at 2,000 chars. Edits that grow
|
||||
past this limit must NOT be silently truncated (the stream consumer
|
||||
would believe the full reply was delivered and stop) and must NOT
|
||||
return failure (the consumer would re-send and create a duplicate).
|
||||
|
||||
Mid-stream (``finalize=False``) we keep editing the original message
|
||||
with a truncated preview — splitting mid-stream would move the edit
|
||||
target to a continuation and the next accumulated-token tick would
|
||||
re-split, looping forever (the Telegram #48648 lesson). The complete
|
||||
text is delivered when ``finalize=True`` via ``_edit_overflow_split``.
|
||||
"""
|
||||
if not self._client:
|
||||
return SendResult(success=False, error="Not connected")
|
||||
try:
|
||||
|
|
@ -2109,14 +2121,159 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
channel = await self._client.fetch_channel(int(chat_id))
|
||||
msg = await channel.fetch_message(int(message_id))
|
||||
formatted = self.format_message(content)
|
||||
|
||||
# Pre-flight: oversized payload. Final edits split-and-deliver;
|
||||
# streaming edits truncate a one-message preview in place.
|
||||
if len(formatted) > self.MAX_MESSAGE_LENGTH:
|
||||
formatted = formatted[:self.MAX_MESSAGE_LENGTH - 3] + "..."
|
||||
await msg.edit(content=formatted)
|
||||
if finalize:
|
||||
return await self._edit_overflow_split(
|
||||
channel, msg, message_id, content,
|
||||
)
|
||||
formatted = self.truncate_message(
|
||||
formatted, self.MAX_MESSAGE_LENGTH,
|
||||
)[0]
|
||||
|
||||
try:
|
||||
await msg.edit(content=formatted)
|
||||
except Exception as edit_err:
|
||||
# Reactive split-and-deliver: format_message inflation (or a
|
||||
# server-side rule change) can push the payload past 2,000
|
||||
# even when the pre-flight check passed. Discord reports this
|
||||
# as "error code: 50035 ... Must be 2000 or fewer in length".
|
||||
if self._is_length_overflow_error(edit_err):
|
||||
if finalize:
|
||||
return await self._edit_overflow_split(
|
||||
channel, msg, message_id, content,
|
||||
)
|
||||
# Mid-stream: truncate and retry in place (no split).
|
||||
truncated = self.truncate_message(
|
||||
formatted, self.MAX_MESSAGE_LENGTH,
|
||||
)[0]
|
||||
await msg.edit(content=truncated)
|
||||
else:
|
||||
raise
|
||||
return SendResult(success=True, message_id=message_id)
|
||||
except Exception as e: # pragma: no cover - defensive logging
|
||||
logger.error("[%s] Failed to edit Discord message %s: %s", self.name, message_id, e, exc_info=True)
|
||||
return SendResult(success=False, error=str(e))
|
||||
|
||||
@staticmethod
|
||||
def _is_length_overflow_error(err: Exception) -> bool:
|
||||
"""True when a Discord edit/send failed because text exceeded 2,000.
|
||||
|
||||
Discord returns ``error code: 50035`` with a ``Must be 2000 or fewer
|
||||
in length`` validation detail. We match on the stable error code plus
|
||||
the length phrasing so unrelated 50035 validation errors (e.g. a bad
|
||||
reply reference) don't get mistaken for an overflow.
|
||||
"""
|
||||
text = str(err).lower()
|
||||
return "error code: 50035" in text and (
|
||||
"2000 or fewer" in text or "fewer in length" in text
|
||||
)
|
||||
|
||||
async def _edit_overflow_split(
|
||||
self,
|
||||
channel: Any,
|
||||
msg: Any,
|
||||
message_id: str,
|
||||
content: str,
|
||||
) -> SendResult:
|
||||
"""Deliver an oversized final edit across message + continuations.
|
||||
|
||||
Edit the original ``message_id`` with chunk 1 (fence-aware, with the
|
||||
usual ``(1/N)`` indicator), then send chunks 2..N as new messages each
|
||||
threaded as a reply to the previous chunk so Discord groups them
|
||||
visually. Returns ``SendResult(success=True, message_id=<last-id>,
|
||||
continuation_message_ids=(...))`` so the stream consumer keeps editing
|
||||
the most recent visible message and can clean up every chunk on a
|
||||
fresh-final.
|
||||
|
||||
On a mid-stream continuation send failure we still report success with
|
||||
however many continuations landed AND a ``partial_overflow``
|
||||
raw_response so the consumer can deliver the missing tail rather than
|
||||
treating a clipped reply as complete — dropping chunks the user already
|
||||
saw would be the worse outcome. Only a first-chunk edit failure
|
||||
returns ``success=False`` (a real adapter problem, not overflow).
|
||||
"""
|
||||
formatted = self.format_message(content)
|
||||
chunks = self.truncate_message(formatted, self.MAX_MESSAGE_LENGTH)
|
||||
if len(chunks) <= 1:
|
||||
# Defensive: caller's pre-flight should guarantee >1 chunk, but if
|
||||
# not, just edit normally.
|
||||
await msg.edit(content=chunks[0] if chunks else formatted)
|
||||
return SendResult(success=True, message_id=message_id)
|
||||
|
||||
# Step 1 — edit the existing message with the first chunk.
|
||||
try:
|
||||
await msg.edit(content=chunks[0])
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"[%s] Overflow split: first-chunk edit failed: %s",
|
||||
self.name, e, exc_info=True,
|
||||
)
|
||||
return SendResult(success=False, error=str(e))
|
||||
|
||||
# Step 2 — send each remaining chunk threaded as a reply to the prior.
|
||||
continuation_ids: list[str] = []
|
||||
delivered = 1
|
||||
prev_msg = msg
|
||||
for chunk in chunks[1:]:
|
||||
reference = None
|
||||
if hasattr(prev_msg, "to_reference"):
|
||||
try:
|
||||
reference = prev_msg.to_reference(fail_if_not_exists=False)
|
||||
except Exception:
|
||||
reference = None
|
||||
try:
|
||||
sent = await channel.send(content=chunk, reference=reference)
|
||||
except Exception as send_err:
|
||||
# Drop the reply anchor and retry once — a deleted/expired
|
||||
# anchor (10008) or system-message reply (50035) shouldn't lose
|
||||
# the chunk.
|
||||
logger.warning(
|
||||
"[%s] Overflow continuation send failed (%s); retrying without reply reference",
|
||||
self.name, send_err,
|
||||
)
|
||||
try:
|
||||
sent = await channel.send(content=chunk, reference=None)
|
||||
except Exception as retry_err:
|
||||
logger.warning(
|
||||
"[%s] Overflow split: stopped at %d/%d chunks delivered: %s",
|
||||
self.name, delivered, len(chunks), retry_err,
|
||||
)
|
||||
last_id = continuation_ids[-1] if continuation_ids else message_id
|
||||
return SendResult(
|
||||
success=True,
|
||||
message_id=last_id,
|
||||
continuation_message_ids=tuple(continuation_ids),
|
||||
raw_response={
|
||||
"partial_overflow": True,
|
||||
"delivered_chunks": delivered,
|
||||
"total_chunks": len(chunks),
|
||||
"last_message_id": last_id,
|
||||
"continuation_message_ids": tuple(continuation_ids),
|
||||
},
|
||||
)
|
||||
new_id = str(sent.id)
|
||||
continuation_ids.append(new_id)
|
||||
delivered += 1
|
||||
prev_msg = sent
|
||||
|
||||
last_id = continuation_ids[-1] if continuation_ids else message_id
|
||||
# Keep the history-backfill fast path pointed at the final visible
|
||||
# chunk so a later non-streaming send threads below the full reply.
|
||||
if not _looks_like_nonconversational_history_message(content):
|
||||
self._last_self_message_id[str(channel.id)] = last_id
|
||||
logger.debug(
|
||||
"[%s] Overflow split delivered %d chunks; last_id=%s",
|
||||
self.name, delivered, last_id,
|
||||
)
|
||||
return SendResult(
|
||||
success=True,
|
||||
message_id=last_id,
|
||||
continuation_message_ids=tuple(continuation_ids),
|
||||
)
|
||||
|
||||
async def _send_file_attachment(
|
||||
self,
|
||||
chat_id: str,
|
||||
|
|
|
|||
343
tests/gateway/test_discord_edit_message_overflow.py
Normal file
343
tests/gateway/test_discord_edit_message_overflow.py
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
"""Regression tests for Discord oversized edit_message split-and-deliver.
|
||||
|
||||
Issue #27881 surfaced as silent truncation: ``edit_message`` clipped any
|
||||
formatted payload over Discord's 2,000-char cap to ``[:1997] + "..."`` and
|
||||
returned ``success=True``, so the stream consumer believed the full reply
|
||||
landed and the user lost everything past the boundary.
|
||||
|
||||
The fix mirrors the proven Telegram contract (and its #48648 lesson):
|
||||
|
||||
* **Mid-stream** (``finalize=False``) — never split. A mid-stream split moves
|
||||
the edit target to a continuation; the next accumulated-token tick re-edits
|
||||
the full text into it and re-splits, looping forever. We truncate a
|
||||
one-message preview in place instead.
|
||||
* **Final** (``finalize=True``) — split-and-deliver: edit chunk 1 in place,
|
||||
send chunks 2..N as reply-threaded continuations, return the LAST visible id
|
||||
in ``message_id`` plus every continuation in ``continuation_message_ids``.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
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", (), {})
|
||||
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 plugins.platforms.discord.adapter import DiscordAdapter # noqa: E402
|
||||
|
||||
|
||||
MAX = DiscordAdapter.MAX_MESSAGE_LENGTH # 2000
|
||||
|
||||
|
||||
def _make_adapter():
|
||||
return DiscordAdapter(PlatformConfig(enabled=True, token="***"))
|
||||
|
||||
|
||||
def _wire_channel(adapter, *, original_msg, send_side_effect=None):
|
||||
"""Wire a fake client whose channel returns ``original_msg`` on fetch and
|
||||
records every ``channel.send`` call."""
|
||||
sends = []
|
||||
|
||||
async def fake_send(*, content, reference=None):
|
||||
sends.append({"content": content, "reference": reference})
|
||||
if send_side_effect is not None:
|
||||
res = send_side_effect(len(sends), content, reference)
|
||||
if res is not None:
|
||||
return res
|
||||
return SimpleNamespace(id=9000 + len(sends))
|
||||
|
||||
channel = SimpleNamespace(
|
||||
id=555,
|
||||
fetch_message=AsyncMock(return_value=original_msg),
|
||||
send=AsyncMock(side_effect=fake_send),
|
||||
)
|
||||
adapter._client = SimpleNamespace(
|
||||
get_channel=lambda _cid: channel,
|
||||
fetch_channel=AsyncMock(return_value=channel),
|
||||
)
|
||||
return channel, sends
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Happy path — short edits unchanged
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class TestEditMessageHappyPath:
|
||||
@pytest.mark.asyncio
|
||||
async def test_short_edit_in_place(self):
|
||||
adapter = _make_adapter()
|
||||
edits = []
|
||||
msg = SimpleNamespace(
|
||||
id=42,
|
||||
edit=AsyncMock(side_effect=lambda *, content: edits.append(content)),
|
||||
)
|
||||
channel, sends = _wire_channel(adapter, original_msg=msg)
|
||||
|
||||
result = await adapter.edit_message("555", "42", "short reply")
|
||||
|
||||
assert result.success is True
|
||||
assert result.message_id == "42"
|
||||
assert result.continuation_message_ids == ()
|
||||
assert edits == ["short reply"]
|
||||
assert sends == [] # no continuations for a short edit
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_client_returns_failure(self):
|
||||
adapter = _make_adapter()
|
||||
adapter._client = None
|
||||
result = await adapter.edit_message("555", "42", "x")
|
||||
assert result.success is False
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Mid-stream overflow — TRUNCATE, never split (the #48648 lesson)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class TestMidStreamOverflowTruncates:
|
||||
@pytest.mark.asyncio
|
||||
async def test_oversized_streaming_edit_truncates_in_place(self):
|
||||
adapter = _make_adapter()
|
||||
edits = []
|
||||
msg = SimpleNamespace(
|
||||
id=42,
|
||||
edit=AsyncMock(side_effect=lambda *, content: edits.append(content)),
|
||||
)
|
||||
channel, sends = _wire_channel(adapter, original_msg=msg)
|
||||
|
||||
big = "p" * 6000
|
||||
result = await adapter.edit_message("555", "42", big, finalize=False)
|
||||
|
||||
# No split: the original message stays the target, no continuations.
|
||||
assert result.success is True
|
||||
assert result.message_id == "42"
|
||||
assert result.continuation_message_ids == ()
|
||||
assert sends == [], "mid-stream overflow must NOT create continuations"
|
||||
# Exactly one in-place edit, clipped to a single chunk under the cap.
|
||||
assert len(edits) == 1
|
||||
assert len(edits[0]) <= MAX
|
||||
# No literal "..." truncation marker leaks in (fence-aware truncation).
|
||||
assert not edits[0].endswith("...")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Final overflow — SPLIT and deliver every chunk
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class TestFinalOverflowSplits:
|
||||
@pytest.mark.asyncio
|
||||
async def test_oversized_final_edit_splits_and_delivers(self):
|
||||
adapter = _make_adapter()
|
||||
edits = []
|
||||
msg = SimpleNamespace(
|
||||
id=42,
|
||||
to_reference=MagicMock(return_value=SimpleNamespace(kind="ref")),
|
||||
edit=AsyncMock(side_effect=lambda *, content: edits.append(content)),
|
||||
)
|
||||
channel, sends = _wire_channel(adapter, original_msg=msg)
|
||||
|
||||
big = "q" * 6000 # ~3-4 chunks at 2000 cap
|
||||
result = await adapter.edit_message("555", "42", big, finalize=True)
|
||||
|
||||
assert result.success is True
|
||||
# message_id points at the LAST visible continuation, not the original.
|
||||
assert result.continuation_message_ids, "expected continuations"
|
||||
assert result.message_id == result.continuation_message_ids[-1]
|
||||
# chunk 1 edited in place; chunks 2..N sent as new messages.
|
||||
assert len(edits) == 1
|
||||
assert len(sends) == len(result.continuation_message_ids)
|
||||
# Every delivered chunk is under the cap.
|
||||
for c in edits + [s["content"] for s in sends]:
|
||||
assert len(c) <= MAX
|
||||
# No "..." truncation marker anywhere.
|
||||
for c in edits + [s["content"] for s in sends]:
|
||||
assert not c.endswith("...")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_byte_coverage_preserved(self):
|
||||
adapter = _make_adapter()
|
||||
edits = []
|
||||
msg = SimpleNamespace(
|
||||
id=42,
|
||||
to_reference=MagicMock(return_value=object()),
|
||||
edit=AsyncMock(side_effect=lambda *, content: edits.append(content)),
|
||||
)
|
||||
channel, sends = _wire_channel(adapter, original_msg=msg)
|
||||
|
||||
# Distinctive marker at the very end must survive end-to-end.
|
||||
body = "a" * 5000 + "END_MARKER_XYZ"
|
||||
result = await adapter.edit_message("555", "42", body, finalize=True)
|
||||
|
||||
assert result.success is True
|
||||
delivered = "".join(edits + [s["content"] for s in sends])
|
||||
assert "END_MARKER_XYZ" in delivered
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_continuations_threaded_as_replies(self):
|
||||
adapter = _make_adapter()
|
||||
msg = SimpleNamespace(
|
||||
id=42,
|
||||
to_reference=MagicMock(return_value=SimpleNamespace(tag="orig")),
|
||||
edit=AsyncMock(),
|
||||
)
|
||||
# Each sent continuation must also expose to_reference so the NEXT
|
||||
# chunk can thread under it.
|
||||
channel, sends = _wire_channel(
|
||||
adapter,
|
||||
original_msg=msg,
|
||||
send_side_effect=lambda n, content, ref: SimpleNamespace(
|
||||
id=9000 + n,
|
||||
to_reference=MagicMock(return_value=SimpleNamespace(tag=f"c{n}")),
|
||||
),
|
||||
)
|
||||
|
||||
result = await adapter.edit_message("555", "42", "z" * 6000, finalize=True)
|
||||
|
||||
assert result.success is True
|
||||
# First continuation replies to the original message's reference.
|
||||
assert sends[0]["reference"] is not None
|
||||
# Later continuations reply to the previous continuation, not None.
|
||||
for s in sends[1:]:
|
||||
assert s["reference"] is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_first_chunk_edit_failure_propagates(self):
|
||||
adapter = _make_adapter()
|
||||
msg = SimpleNamespace(
|
||||
id=42,
|
||||
to_reference=MagicMock(return_value=object()),
|
||||
edit=AsyncMock(side_effect=RuntimeError("hard edit failure")),
|
||||
)
|
||||
channel, sends = _wire_channel(adapter, original_msg=msg)
|
||||
|
||||
result = await adapter.edit_message("555", "42", "w" * 6000, finalize=True)
|
||||
|
||||
assert result.success is False
|
||||
assert "hard edit failure" in (result.error or "")
|
||||
assert sends == [] # never reached the continuation loop
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mid_continuation_failure_reports_partial(self):
|
||||
adapter = _make_adapter()
|
||||
msg = SimpleNamespace(
|
||||
id=42,
|
||||
to_reference=MagicMock(return_value=object()),
|
||||
edit=AsyncMock(),
|
||||
)
|
||||
|
||||
# First continuation succeeds; second fails both with and without ref.
|
||||
def side(n, content, ref):
|
||||
if n == 1:
|
||||
return SimpleNamespace(id=9001, to_reference=MagicMock(return_value=object()))
|
||||
raise RuntimeError("continuation send failed")
|
||||
|
||||
channel, sends = _wire_channel(adapter, original_msg=msg, send_side_effect=side)
|
||||
|
||||
result = await adapter.edit_message("555", "42", "k" * 6000, finalize=True)
|
||||
|
||||
# Partial delivery still reports success (don't drop chunks the user
|
||||
# already saw) but flags partial_overflow so the consumer retries tail.
|
||||
assert result.success is True
|
||||
assert result.raw_response["partial_overflow"] is True
|
||||
assert result.raw_response["delivered_chunks"] < result.raw_response["total_chunks"]
|
||||
assert result.message_id == "9001"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Reactive overflow — Discord 50035 mid-edit triggers the same branch logic
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class TestReactiveOverflowDetection:
|
||||
@pytest.mark.asyncio
|
||||
async def test_50035_on_final_edit_triggers_split(self):
|
||||
adapter = _make_adapter()
|
||||
edit_calls = []
|
||||
|
||||
# format_message leaves content under the cap, but the first edit
|
||||
# raises 50035 (server-side rejection); the split path then runs.
|
||||
def edit_effect(*, content):
|
||||
edit_calls.append(content)
|
||||
if len(edit_calls) == 1:
|
||||
raise RuntimeError(
|
||||
"400 Bad Request (error code: 50035): Invalid Form Body\n"
|
||||
"In content: Must be 2000 or fewer in length."
|
||||
)
|
||||
|
||||
msg = SimpleNamespace(
|
||||
id=42,
|
||||
to_reference=MagicMock(return_value=object()),
|
||||
edit=AsyncMock(side_effect=edit_effect),
|
||||
)
|
||||
channel, sends = _wire_channel(adapter, original_msg=msg)
|
||||
|
||||
# Content is UNDER the cap so pre-flight passes; the 50035 on edit
|
||||
# forces the reactive split.
|
||||
result = await adapter.edit_message("555", "42", "u" * 1500, finalize=True)
|
||||
|
||||
assert result.success is True
|
||||
# Reactive split re-edited chunk 1 and may add continuations.
|
||||
assert len(edit_calls) >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unrelated_50035_is_not_treated_as_overflow(self):
|
||||
adapter = _make_adapter()
|
||||
msg = SimpleNamespace(
|
||||
id=42,
|
||||
edit=AsyncMock(side_effect=RuntimeError(
|
||||
"400 Bad Request (error code: 50035): In message_reference: "
|
||||
"Cannot reply to a system message"
|
||||
)),
|
||||
)
|
||||
channel, sends = _wire_channel(adapter, original_msg=msg)
|
||||
|
||||
result = await adapter.edit_message("555", "42", "small", finalize=True)
|
||||
|
||||
# Not a length error → propagates as a normal failure, no split.
|
||||
assert result.success is False
|
||||
assert sends == []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Overflow detector helper
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class TestLengthOverflowDetector:
|
||||
def test_matches_length_50035(self):
|
||||
err = RuntimeError(
|
||||
"error code: 50035 ... Must be 2000 or fewer in length."
|
||||
)
|
||||
assert DiscordAdapter._is_length_overflow_error(err) is True
|
||||
|
||||
def test_ignores_non_length_50035(self):
|
||||
err = RuntimeError("error code: 50035: Cannot reply to a system message")
|
||||
assert DiscordAdapter._is_length_overflow_error(err) is False
|
||||
|
||||
def test_ignores_other_errors(self):
|
||||
assert DiscordAdapter._is_length_overflow_error(RuntimeError("timeout")) is False
|
||||
Loading…
Add table
Add a link
Reference in a new issue