Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
"""Regression tests for the Discord split-delivery cap (issue #86581).
|
||||
|
||||
A degenerate turn can produce tens of thousands of characters. Without a
|
||||
ceiling, the adapter posts every 2000-char chunk back-to-back and floods the
|
||||
channel — the #86581 incident delivered 60,698 chars as 31 messages. The
|
||||
cap keeps the first ``MAX_SPLIT_MESSAGES`` chunks and replaces the remainder
|
||||
with a short notice.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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
|
||||
CAP = DiscordAdapter.MAX_SPLIT_MESSAGES
|
||||
|
||||
|
||||
def _make_adapter():
|
||||
return DiscordAdapter(PlatformConfig(enabled=True, token="***"))
|
||||
|
||||
|
||||
def _huge_content(chars: int = 60_000) -> str:
|
||||
# Distinct filler — this test is about SIZE, not repetition.
|
||||
return " ".join(f"word-{i}-" + "x" * 12 for i in range(chars // 20))
|
||||
|
||||
|
||||
class TestCapSplitChunks:
|
||||
def test_below_cap_unchanged(self):
|
||||
adapter = _make_adapter()
|
||||
chunks = ["a", "b", "c"]
|
||||
assert adapter._cap_split_chunks(chunks) == chunks
|
||||
|
||||
def test_over_cap_keeps_n_minus_1_plus_notice(self):
|
||||
adapter = _make_adapter()
|
||||
chunks = [f"chunk-{i}-" + "z" * 100 for i in range(40)]
|
||||
capped = adapter._cap_split_chunks(chunks)
|
||||
assert len(capped) == CAP
|
||||
assert capped[0] == chunks[0]
|
||||
assert "Response truncated" in capped[-1]
|
||||
assert "delivery limit" in capped[-1]
|
||||
# The notice itself must stay under Discord's per-message cap.
|
||||
assert len(capped[-1]) <= MAX
|
||||
|
||||
|
||||
class TestSendCap:
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_caps_split_flood(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
adapter = _make_adapter()
|
||||
sends = []
|
||||
|
||||
async def fake_send(*, content, reference=None):
|
||||
sends.append(content)
|
||||
return SimpleNamespace(id=9000 + len(sends))
|
||||
|
||||
channel = SimpleNamespace(id=555, send=AsyncMock(side_effect=fake_send))
|
||||
adapter._client = SimpleNamespace(
|
||||
get_channel=lambda _cid: channel,
|
||||
fetch_channel=AsyncMock(),
|
||||
)
|
||||
|
||||
result = await adapter.send("555", _huge_content())
|
||||
|
||||
assert result.success is True
|
||||
assert len(sends) == CAP
|
||||
assert "Response truncated" in sends[-1]
|
||||
|
||||
|
||||
class TestForumCap:
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_to_forum_caps_followup_chunks(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
adapter = _make_adapter()
|
||||
thread_sends = []
|
||||
|
||||
async def fake_thread_send(*, content):
|
||||
thread_sends.append(content)
|
||||
return SimpleNamespace(id=8000 + len(thread_sends))
|
||||
|
||||
thread_channel = SimpleNamespace(
|
||||
id=777, send=AsyncMock(side_effect=fake_thread_send)
|
||||
)
|
||||
forum_channel = SimpleNamespace(
|
||||
id=666,
|
||||
type=SimpleNamespace(value=15),
|
||||
create_thread=AsyncMock(return_value=SimpleNamespace(
|
||||
id=777,
|
||||
thread=thread_channel,
|
||||
message=SimpleNamespace(id=8000),
|
||||
)),
|
||||
)
|
||||
|
||||
result = await adapter._send_to_forum(forum_channel, _huge_content())
|
||||
|
||||
assert result.success is True
|
||||
# 1 starter message + at most (CAP - 1) follow-up chunks.
|
||||
assert len(thread_sends) <= CAP - 1
|
||||
assert "Response truncated" in thread_sends[-1]
|
||||
|
||||
|
||||
class TestEditOverflowCap:
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_overflow_split_capped(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
adapter = _make_adapter()
|
||||
edits = []
|
||||
sends = []
|
||||
|
||||
async def fake_edit(*, content):
|
||||
edits.append(content)
|
||||
|
||||
async def fake_send(*, content, reference=None):
|
||||
sends.append(content)
|
||||
return SimpleNamespace(id=9000 + len(sends))
|
||||
|
||||
msg = SimpleNamespace(id=42, edit=AsyncMock(side_effect=fake_edit))
|
||||
channel = SimpleNamespace(id=555, send=AsyncMock(side_effect=fake_send))
|
||||
|
||||
result = await adapter._edit_overflow_split(channel, msg, "42", _huge_content())
|
||||
|
||||
assert result.success is True
|
||||
# 1 in-place edit + at most (CAP - 1) continuation sends.
|
||||
assert len(edits) == 1
|
||||
assert len(sends) <= CAP - 1
|
||||
assert "Response truncated" in (sends[-1] if sends else edits[-1])
|
||||
Reference in New Issue
Block a user