mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 15:01:34 +08:00
discord.py does not apply a default AllowedMentions to the client, so any reply whose content contains @everyone/@here or a role mention would ping the whole server — including verbatim echoes of user input or LLM output that happens to contain those tokens. Set a safe default on commands.Bot: everyone=False, roles=False, users=True, replied_user=True. Operators can opt back in via four DISCORD_ALLOW_MENTION_* env vars or discord.allow_mentions.* in config.yaml. No behavior change for normal user/reply pings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
156 lines
6.0 KiB
Python
156 lines
6.0 KiB
Python
"""Tests for the Discord ``allowed_mentions`` safe-default helper.
|
|
|
|
Ensures the bot defaults to blocking ``@everyone`` / ``@here`` / role pings
|
|
so an LLM response (or echoed user content) can't spam a whole server —
|
|
and that the four ``DISCORD_ALLOW_MENTION_*`` env vars correctly opt back
|
|
in when an operator explicitly wants a different policy.
|
|
"""
|
|
|
|
import sys
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
|
|
class _FakeAllowedMentions:
|
|
"""Stand-in for ``discord.AllowedMentions`` that exposes the same four
|
|
boolean flags as real attributes so the test can assert on them.
|
|
"""
|
|
|
|
def __init__(self, *, everyone=True, roles=True, users=True, replied_user=True):
|
|
self.everyone = everyone
|
|
self.roles = roles
|
|
self.users = users
|
|
self.replied_user = replied_user
|
|
|
|
def __repr__(self) -> str: # pragma: no cover - debug helper
|
|
return (
|
|
f"AllowedMentions(everyone={self.everyone}, roles={self.roles}, "
|
|
f"users={self.users}, replied_user={self.replied_user})"
|
|
)
|
|
|
|
|
|
def _ensure_discord_mock():
|
|
"""Install (or augment) a mock ``discord`` module.
|
|
|
|
Other test modules in this directory stub ``discord`` via
|
|
``sys.modules.setdefault`` — whichever test file imports first wins and
|
|
our full module is then silently dropped. We therefore ALWAYS force
|
|
``AllowedMentions`` onto whatever is currently in ``sys.modules["discord"]``;
|
|
that's the only attribute this test file actually needs real behavior from.
|
|
"""
|
|
if "discord" in sys.modules and hasattr(sys.modules["discord"], "__file__"):
|
|
sys.modules["discord"].AllowedMentions = _FakeAllowedMentions
|
|
return
|
|
|
|
if sys.modules.get("discord") is None:
|
|
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, grey=4, secondary=5)
|
|
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),
|
|
choices=lambda **kwargs: (lambda fn: fn),
|
|
Choice=lambda **kwargs: SimpleNamespace(**kwargs),
|
|
)
|
|
discord_mod.opus = SimpleNamespace(is_loaded=lambda: True)
|
|
|
|
ext_mod = MagicMock()
|
|
commands_mod = MagicMock()
|
|
commands_mod.Bot = MagicMock
|
|
ext_mod.commands = commands_mod
|
|
|
|
sys.modules["discord"] = discord_mod
|
|
sys.modules.setdefault("discord.ext", ext_mod)
|
|
sys.modules.setdefault("discord.ext.commands", commands_mod)
|
|
|
|
# Whether we just installed the mock OR the mock was already installed
|
|
# by another test's _ensure_discord_mock, force the AllowedMentions
|
|
# stand-in onto it — _build_allowed_mentions() reads this attribute.
|
|
sys.modules["discord"].AllowedMentions = _FakeAllowedMentions
|
|
|
|
|
|
_ensure_discord_mock()
|
|
|
|
from gateway.platforms.discord import _build_allowed_mentions # noqa: E402
|
|
|
|
|
|
# The four DISCORD_ALLOW_MENTION_* env vars that _build_allowed_mentions reads.
|
|
# Cleared before each test so env leakage from other tests never masks a regression.
|
|
_ENV_VARS = (
|
|
"DISCORD_ALLOW_MENTION_EVERYONE",
|
|
"DISCORD_ALLOW_MENTION_ROLES",
|
|
"DISCORD_ALLOW_MENTION_USERS",
|
|
"DISCORD_ALLOW_MENTION_REPLIED_USER",
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_allowed_mention_env(monkeypatch):
|
|
for name in _ENV_VARS:
|
|
monkeypatch.delenv(name, raising=False)
|
|
|
|
|
|
def test_safe_defaults_block_everyone_and_roles():
|
|
am = _build_allowed_mentions()
|
|
assert am.everyone is False, "default must NOT allow @everyone/@here pings"
|
|
assert am.roles is False, "default must NOT allow role pings"
|
|
assert am.users is True, "default must allow user pings so replies work"
|
|
assert am.replied_user is True, "default must allow reply-reference pings"
|
|
|
|
|
|
def test_env_var_opts_back_into_everyone(monkeypatch):
|
|
monkeypatch.setenv("DISCORD_ALLOW_MENTION_EVERYONE", "true")
|
|
am = _build_allowed_mentions()
|
|
assert am.everyone is True
|
|
# other defaults unaffected
|
|
assert am.roles is False
|
|
assert am.users is True
|
|
assert am.replied_user is True
|
|
|
|
|
|
def test_env_var_can_disable_users(monkeypatch):
|
|
monkeypatch.setenv("DISCORD_ALLOW_MENTION_USERS", "false")
|
|
am = _build_allowed_mentions()
|
|
assert am.users is False
|
|
# safe defaults elsewhere remain
|
|
assert am.everyone is False
|
|
assert am.roles is False
|
|
assert am.replied_user is True
|
|
|
|
|
|
@pytest.mark.parametrize("raw, expected", [
|
|
("true", True), ("True", True), ("TRUE", True),
|
|
("1", True), ("yes", True), ("YES", True), ("on", True),
|
|
("false", False), ("False", False), ("0", False),
|
|
("no", False), ("off", False),
|
|
("", False), # empty falls back to default (False for everyone)
|
|
("garbage", False), # unknown falls back to default
|
|
(" true ", True), # whitespace tolerated
|
|
])
|
|
def test_everyone_boolean_parsing(monkeypatch, raw, expected):
|
|
monkeypatch.setenv("DISCORD_ALLOW_MENTION_EVERYONE", raw)
|
|
am = _build_allowed_mentions()
|
|
assert am.everyone is expected
|
|
|
|
|
|
def test_all_four_knobs_together(monkeypatch):
|
|
monkeypatch.setenv("DISCORD_ALLOW_MENTION_EVERYONE", "true")
|
|
monkeypatch.setenv("DISCORD_ALLOW_MENTION_ROLES", "true")
|
|
monkeypatch.setenv("DISCORD_ALLOW_MENTION_USERS", "false")
|
|
monkeypatch.setenv("DISCORD_ALLOW_MENTION_REPLIED_USER", "false")
|
|
am = _build_allowed_mentions()
|
|
assert am.everyone is True
|
|
assert am.roles is True
|
|
assert am.users is False
|
|
assert am.replied_user is False
|