Files
nousresearch--hermes-agent/tests/e2e/test_discord_adapter.py
T
wehub-resource-sync b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:56:03 +08:00

156 lines
5.8 KiB
Python

"""Minimal e2e tests for Discord mention stripping + /command detection.
Covers the fix for slash commands not being recognized when sent via
@mention in a channel, especially after auto-threading.
"""
import asyncio
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from tests.e2e.conftest import (
BOT_USER_ID,
E2E_MESSAGE_SETTLE_DELAY,
get_response_text,
make_discord_message,
make_fake_dm_channel,
make_fake_thread,
)
pytestmark = pytest.mark.asyncio
async def dispatch(adapter, msg):
await adapter._handle_message(msg)
await asyncio.sleep(E2E_MESSAGE_SETTLE_DELAY)
class TestMentionStrippedCommandDispatch:
async def test_mention_then_command(self, discord_adapter, bot_user):
"""<@BOT> /help → mention stripped, /help dispatched."""
msg = make_discord_message(
content=f"<@{BOT_USER_ID}> /help",
mentions=[bot_user],
)
await dispatch(discord_adapter, msg)
response = get_response_text(discord_adapter)
assert response is not None
assert "/new" in response
async def test_nickname_mention_then_command(self, discord_adapter, bot_user):
"""<@!BOT> /help → nickname mention also stripped, /help works."""
msg = make_discord_message(
content=f"<@!{BOT_USER_ID}> /help",
mentions=[bot_user],
)
await dispatch(discord_adapter, msg)
response = get_response_text(discord_adapter)
assert response is not None
assert "/new" in response
async def test_text_before_command_not_detected(self, discord_adapter, bot_user):
"""'<@BOT> something else /help' → mention stripped, but 'something else /help'
doesn't start with / so it's treated as text, not a command."""
msg = make_discord_message(
content=f"<@{BOT_USER_ID}> something else /help",
mentions=[bot_user],
)
await dispatch(discord_adapter, msg)
# Message is accepted (not dropped by mention gate), but since it doesn't
# start with / it's routed as text — no command output, and no agent in this
# mock setup means no send call either.
response = get_response_text(discord_adapter)
assert response is None or "/new" not in response
async def test_no_mention_in_channel_dropped(self, discord_adapter):
"""Message without @mention in server channel → silently dropped."""
msg = make_discord_message(content="/help", mentions=[])
await dispatch(discord_adapter, msg)
assert get_response_text(discord_adapter) is None
async def test_dm_no_mention_needed(self, discord_adapter):
"""DMs don't require @mention — /help works directly."""
dm = make_fake_dm_channel()
msg = make_discord_message(content="/help", channel=dm, mentions=[])
await dispatch(discord_adapter, msg)
response = get_response_text(discord_adapter)
assert response is not None
assert "/new" in response
class TestAutoThreadingPreservesCommand:
async def test_command_detected_after_auto_thread(self, discord_adapter, bot_user, monkeypatch):
"""@mention /help in channel with auto-thread → thread created AND command dispatched."""
monkeypatch.setenv("DISCORD_AUTO_THREAD", "true")
fake_thread = make_fake_thread(thread_id=90001, name="help")
msg = make_discord_message(
content=f"<@{BOT_USER_ID}> /help",
mentions=[bot_user],
)
# Simulate discord.py restoring the original raw content (with mention)
# after create_thread(), which undoes any prior mention stripping.
original_content = msg.content
async def clobber_content(**kwargs):
msg.content = original_content
return fake_thread
msg.create_thread = AsyncMock(side_effect=clobber_content)
await dispatch(discord_adapter, msg)
msg.create_thread.assert_awaited_once()
response = get_response_text(discord_adapter)
assert response is not None
assert "/new" in response
class TestRepliedToMediaDispatch:
async def test_reply_to_image_message_caches_referenced_attachment(
self, discord_adapter, bot_user, monkeypatch
):
"""A text reply to an image-bearing Discord message should give the agent that image."""
cached_path = "/tmp/replied-discord-image.png"
async def fake_cache_image_from_url(url, *, ext=".jpg"):
assert url == "https://cdn.discordapp.com/attachments/image.png"
assert ext == ".png"
return cached_path
monkeypatch.setattr(
"plugins.platforms.discord.adapter.cache_image_from_url",
fake_cache_image_from_url,
)
discord_adapter.handle_message = AsyncMock()
attachment = SimpleNamespace(
content_type="image/png",
filename="image.png",
url="https://cdn.discordapp.com/attachments/image.png",
size=1234,
)
referenced_message = SimpleNamespace(
id=12345,
content="",
attachments=[attachment],
)
msg = make_discord_message(
content=f"<@{BOT_USER_ID}> what's in this image?",
mentions=[bot_user],
)
msg.type = 19
msg.reference = SimpleNamespace(message_id=12345, resolved=referenced_message)
await discord_adapter._handle_message(msg)
discord_adapter.handle_message.assert_awaited_once()
await_args = discord_adapter.handle_message.await_args
assert await_args is not None
event = await_args.args[0]
assert event.reply_to_message_id == "12345"
assert event.media_urls == [cached_path]
assert event.media_types == ["image/png"]
assert event.message_type.value == "photo"