Files
aiturk-hermes-ide/tests/gateway/test_slack_ignore_other_user_mentions.py
T

264 lines
9.0 KiB
Python

"""Tests for the Slack ``ignore_other_user_mentions`` option.
When enabled, the bot stays silent on a channel/thread message that opens by
@-mentioning someone other than itself (a message *addressed to* another user),
unless the bot is also mentioned. This is Slack parity for the Discord option
of the same name (PR #33501), adapted to Slack's thread model: the trigger is a
*leading* mention, so a message that merely references another user mid-sentence
(e.g. "loop in @rasha") still reaches the bot.
Helper-level tests exercise the real config/parse methods directly; the
integration tests drive the real ``SlackAdapter._handle_slack_message`` so the
gate is verified end-to-end rather than against a re-implementation.
"""
import sys
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from gateway.config import Platform, PlatformConfig
# ---------------------------------------------------------------------------
# Mock slack-bolt if not installed (same pattern as test_slack_mention.py)
# ---------------------------------------------------------------------------
def _ensure_slack_mock():
if "slack_bolt" in sys.modules and hasattr(sys.modules["slack_bolt"], "__file__"):
return
slack_bolt = MagicMock()
slack_bolt.async_app.AsyncApp = MagicMock
slack_bolt.adapter.socket_mode.async_handler.AsyncSocketModeHandler = MagicMock
slack_sdk = MagicMock()
slack_sdk.web.async_client.AsyncWebClient = MagicMock
for name, mod in [
("slack_bolt", slack_bolt),
("slack_bolt.async_app", slack_bolt.async_app),
("slack_bolt.adapter", slack_bolt.adapter),
("slack_bolt.adapter.socket_mode", slack_bolt.adapter.socket_mode),
("slack_bolt.adapter.socket_mode.async_handler", slack_bolt.adapter.socket_mode.async_handler),
("slack_sdk", slack_sdk),
("slack_sdk.web", slack_sdk.web),
("slack_sdk.web.async_client", slack_sdk.web.async_client),
]:
sys.modules.setdefault(name, mod)
_ensure_slack_mock()
import plugins.platforms.slack.adapter as _slack_mod # noqa: E402
_slack_mod.SLACK_AVAILABLE = True
from plugins.platforms.slack.adapter import SlackAdapter # noqa: E402
BOT_USER_ID = "U_BOT_123"
OTHER_USER_ID = "U_OTHER_456"
CHANNEL_ID = "C0AQWDLHY9M"
def _make_adapter(**extra):
adapter = object.__new__(SlackAdapter)
adapter.platform = Platform.SLACK
adapter.config = PlatformConfig(enabled=True, extra=dict(extra))
adapter._bot_user_id = BOT_USER_ID
adapter._team_bot_user_ids = {}
return adapter
# ---------------------------------------------------------------------------
# _slack_ignore_other_user_mentions()
# ---------------------------------------------------------------------------
def test_ignore_other_user_mentions_defaults_off(monkeypatch):
monkeypatch.delenv("SLACK_IGNORE_OTHER_USER_MENTIONS", raising=False)
adapter = _make_adapter()
assert adapter._slack_ignore_other_user_mentions() is False
# ---------------------------------------------------------------------------
# _slack_message_addressed_to_other_user()
# ---------------------------------------------------------------------------
SELF_UIDS = {BOT_USER_ID}
def _addressed(text):
return _make_adapter()._slack_message_addressed_to_other_user(text, SELF_UIDS)
def test_addressed_empty_and_blank():
assert _addressed("") is False
assert _addressed(" ") is False
def test_addressed_channel_and_broadcast_tokens_are_not_users():
# <!here>, <!channel>, and channel refs address the room, not a person.
assert _addressed("<!here> standup in 5") is False
assert _addressed("<#C0000000000|general> see here") is False
# ---------------------------------------------------------------------------
# _slack_message_mentions_self()
# ---------------------------------------------------------------------------
def _mentions_self(text):
return _make_adapter()._slack_message_mentions_self(text, SELF_UIDS)
def test_mentions_self_plain_form():
assert _mentions_self(f"hello <@{BOT_USER_ID}>") is True
def test_mentions_self_id_prefix_is_not_a_match():
# <@U_BOT_123X> is a different user whose ID merely starts with ours.
assert _mentions_self(f"hello <@{BOT_USER_ID}X>") is False
# ---------------------------------------------------------------------------
# Integration: real _handle_slack_message
# ---------------------------------------------------------------------------
@pytest.fixture
def adapter():
config = PlatformConfig(enabled=True, token="xoxb-fake-token")
a = SlackAdapter(config)
a._app = MagicMock()
a._app.client = AsyncMock()
a._bot_user_id = BOT_USER_ID
a._running = True
a.handle_message = AsyncMock()
return a
@pytest.fixture(autouse=True)
def _redirect_cache(tmp_path, monkeypatch):
monkeypatch.setattr(
"gateway.platforms.base.DOCUMENT_CACHE_DIR", tmp_path / "doc_cache"
)
# Keep gating driven by config.extra, not ambient env.
monkeypatch.delenv("SLACK_IGNORE_OTHER_USER_MENTIONS", raising=False)
monkeypatch.delenv("SLACK_FREE_RESPONSE_CHANNELS", raising=False)
monkeypatch.delenv("SLACK_REQUIRE_MENTION", raising=False)
def _event(text, ts, thread_ts=None):
event = {
"channel": CHANNEL_ID,
"channel_type": "channel",
"user": "U_HUMAN",
"text": text,
"ts": ts,
}
if thread_ts is not None:
event["thread_ts"] = thread_ts
return event
async def _run(adapter, event):
with patch.object(
adapter, "_resolve_user_name", new=AsyncMock(return_value="human")
), patch.object(
adapter, "_fetch_thread_context", new=AsyncMock(return_value=None)
), patch.object(
adapter, "_fetch_thread_parent_text", new=AsyncMock(return_value="")
), patch.object(
adapter, "_has_active_session_for_thread", return_value=False
):
await adapter._handle_slack_message(event)
@pytest.mark.asyncio
async def test_free_response_replies_when_bot_also_mentioned(adapter):
adapter.config.extra["free_response_channels"] = CHANNEL_ID
adapter.config.extra["ignore_other_user_mentions"] = True
await _run(
adapter,
_event(f"<@{OTHER_USER_ID}> and <@{BOT_USER_ID}> please compare", ts="1700000000.000003"),
)
adapter.handle_message.assert_awaited_once()
@pytest.mark.asyncio
async def test_free_response_replies_when_bot_mentioned_in_pipe_form(adapter):
"""A pipe-form bot mention (``<@U123|name>``) counts as "also mentioned"
even though the exact-markup ``is_mentioned`` check misses it."""
adapter.config.extra["free_response_channels"] = CHANNEL_ID
adapter.config.extra["ignore_other_user_mentions"] = True
await _run(
adapter,
_event(
f"<@{OTHER_USER_ID}|rasha> and <@{BOT_USER_ID}|hermes> please compare",
ts="1700000000.000004",
),
)
adapter.handle_message.assert_awaited_once()
@pytest.mark.asyncio
async def test_mentioned_thread_ignores_followup_addressed_to_other_user(adapter):
"""Ben's case: once the bot has been mentioned in a thread it auto-follows,
but a follow-up addressed to another human should not wake it."""
thread_ts = "1700000000.000010"
adapter._mentioned_threads.add(thread_ts)
adapter.config.extra["ignore_other_user_mentions"] = True
await _run(
adapter,
_event(f"<@{OTHER_USER_ID}> check this out", ts="1700000000.000011", thread_ts=thread_ts),
)
adapter.handle_message.assert_not_awaited()
# ---------------------------------------------------------------------------
# Config bridge: config.yaml slack.ignore_other_user_mentions → extra + env
# ---------------------------------------------------------------------------
def test_config_bridges_slack_ignore_other_user_mentions(monkeypatch, tmp_path):
"""config.yaml ``slack.ignore_other_user_mentions`` reaches the runtime env
var the adapter reads (same wiring as ``strict_mention`` — via the plugin's
apply_yaml_config_fn bridge, not the generic shared-key allowlist)."""
from gateway.config import load_gateway_config
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
(hermes_home / "config.yaml").write_text(
"slack:\n ignore_other_user_mentions: true\n",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.delenv("SLACK_IGNORE_OTHER_USER_MENTIONS", raising=False)
load_gateway_config()
import os as _os
assert _os.environ["SLACK_IGNORE_OTHER_USER_MENTIONS"] == "true"
def test_ignore_other_user_mentions_env_wins_over_yaml(monkeypatch, tmp_path):
from gateway.config import load_gateway_config
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
(hermes_home / "config.yaml").write_text(
"slack:\n ignore_other_user_mentions: true\n",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.setenv("SLACK_IGNORE_OTHER_USER_MENTIONS", "false")
load_gateway_config()
import os as _os
assert _os.environ["SLACK_IGNORE_OTHER_USER_MENTIONS"] == "false"