Files
hermes-agent/tests/gateway/test_slack_thread_require_mention.py
teknium1 3dedb71f2f fix(platforms): adapter settings resolve explicit env → own YAML → default, per profile
One reader (gateway.platforms._shared.extra_or_secret) now implements the
precedence every per-profile setting follows for the OWNING profile:
explicit scoped env/.env → that profile's config.yaml (PlatformConfig.extra)
→ the adapter's default. A scoped miss returns the default, never the launch
process's os.environ; single-profile / default-profile installs keep the
documented env-over-YAML contract.

Why: 545e74d0ea (#108705) stopped bridging a secondary's YAML into the
process env and moved readers to config.extra, but the shared reader and the
hand-rolled helpers in Discord/Slack/Matrix/Telegram consulted YAML FIRST and
then fell back to a scoped env read. Two bug classes followed (#108440
post-merge review by andrexibiza, #109032):
- an explicit env value could no longer beat YAML for the owning profile
  (DISCORD_ALLOW_MENTION_EVERYONE=false lost to allow_mentions.everyone: true;
  TELEGRAM_REACTIONS=true lost to the stock reactions: false);
- a secondary that OMITTED a key inherited the launch profile's bridged env
  through the fallback (Matrix process_notices/session_scope, Discord
  auto_thread/reactions/mentions, Slack reactions/ignored_channels).

Consumers migrated to the shared reader: Discord _build_allowed_mentions and
_extra_or_env_flag; Slack _slack_allow_bots, _reactions_enabled (the
_extra_or_env_* getters already used it); Matrix _extra_truthy, _extra_csv_set,
session_scope, reactions, require_mention parsers, and — new — the
allowed_users / ignore_user_patterns consumers that never read the seeded YAML
lists; Telegram _extra_bool, _extra_str_set, _reactions_enabled; Feishu
allow_bots; WhatsApp dm_policy/group_policy.

Refs #108440, #109032
2026-09-13 15:39:11 -07:00

155 lines
4.2 KiB
Python

import asyncio
import os
from gateway.config import PlatformConfig
from plugins.platforms.slack.adapter import SlackAdapter, _apply_yaml_config
def run(coro):
return asyncio.run(coro)
def make_adapter(extra=None):
config = PlatformConfig(extra=extra or {})
adapter = SlackAdapter(config)
adapter._bot_user_id = "UBOT"
adapter._team_bot_user_ids["T1"] = "UBOT"
adapter._has_active_session_for_thread = lambda **_: False
async def no_thread_context(**_):
return ""
async def no_parent_text(**_):
return ""
async def user_name(*_, **__):
return "Sebastian"
adapter._fetch_thread_context = no_thread_context
adapter._fetch_thread_parent_text = no_parent_text
adapter._resolve_user_name = user_name
return adapter
def slack_event(text, ts="100.000", thread_ts=None):
event = {
"type": "message",
"channel": "C123",
"channel_type": "channel",
"team": "T1",
"user": "U123",
"text": text,
"ts": ts,
}
if thread_ts is not None:
event["thread_ts"] = thread_ts
return event
def test_thread_require_mention_env_bridge(monkeypatch):
monkeypatch.delenv("SLACK_THREAD_REQUIRE_MENTION", raising=False)
_apply_yaml_config(
{},
{
"thread_require_mention": True,
},
)
assert os.environ["SLACK_THREAD_REQUIRE_MENTION"] == "true"
def test_thread_require_mention_parses_yaml_and_env(monkeypatch):
"""Explicit env beats YAML (documented env-over-YAML contract); YAML decides when env is unset."""
monkeypatch.setenv("SLACK_THREAD_REQUIRE_MENTION", "true")
assert make_adapter()._slack_thread_require_mention() is True
assert make_adapter({"thread_require_mention": "false"})._slack_thread_require_mention() is True
monkeypatch.setenv("SLACK_THREAD_REQUIRE_MENTION", "false")
assert make_adapter({"thread_require_mention": True})._slack_thread_require_mention() is False
monkeypatch.delenv("SLACK_THREAD_REQUIRE_MENTION", raising=False)
assert make_adapter({"thread_require_mention": "false"})._slack_thread_require_mention() is False
assert make_adapter({"thread_require_mention": True})._slack_thread_require_mention() is True
def test_thread_require_mention_allows_top_level_free_response():
adapter = make_adapter(
{
"allowed_channels": ["C123"],
"require_mention": False,
"thread_require_mention": True,
"reply_in_thread": True,
}
)
handled = []
async def capture(event):
handled.append(event)
adapter.handle_message = capture
run(adapter._handle_slack_message(slack_event("vpn is broken", ts="100.000")))
assert len(handled) == 1
assert handled[0].text == "vpn is broken"
assert handled[0].source.thread_id == "100.000"
def test_thread_require_mention_blocks_unmentioned_thread_reply():
adapter = make_adapter(
{
"allowed_channels": ["C123"],
"require_mention": False,
"thread_require_mention": True,
"reply_in_thread": True,
}
)
handled = []
async def capture(event):
handled.append(event)
adapter.handle_message = capture
run(
adapter._handle_slack_message(
slack_event("we found another 403", ts="101.000", thread_ts="100.000")
)
)
assert handled == []
def test_thread_require_mention_allows_mentioned_thread_reply_without_sticky_thread():
adapter = make_adapter(
{
"allowed_channels": ["C123"],
"require_mention": False,
"thread_require_mention": True,
"reply_in_thread": True,
}
)
handled = []
async def capture(event):
handled.append(event)
adapter.handle_message = capture
run(
adapter._handle_slack_message(
slack_event("<@UBOT> update this", ts="101.000", thread_ts="100.000")
)
)
assert len(handled) == 1
assert handled[0].text == "update this"
assert "100.000" not in adapter._mentioned_threads
run(
adapter._handle_slack_message(
slack_event("follow-up without mention", ts="102.000", thread_ts="100.000")
)
)
assert len(handled) == 1