hermes-agent/tests/gateway/test_whatsapp_group_gating.py
Teknium 6b81590c55
test: prune low-value tests suite-wide (wave 1) — 46,820 → 28,106 test functions
Systematic prune per AGENTS.md test policy, one pass over every major
test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli,
cron, tui_gateway, honcho/openviking, root-level):

- DELETE: source-reading tests (read_text/getsource on prod files),
  change-detector tests (exact catalog counts, model-name snapshots,
  config version literals), mock-echo tests (assert a mock returns what
  it was told), assertion-free/trivial tests, near-duplicate
  parametrizations (boundaries + one representative kept), async/sync
  twin duplicates, cosmetic within-file variations.
- KEEP (mandatory): security/redaction/approval guards, message-role
  alternation invariants, prompt-caching/deterministic-call-id
  invariants, issue-number regression tests (deduped), E2E tests.
- 6 test files deleted outright (script-style/no-assert or fully
  redundant); conftest.py, fakes/, fixtures/ untouched.
- tests/acp/conftest.py added: autouse fixture stubs the live
  models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server
  tests performed on every session create — test_server.py 147s → 3.4s,
  and the tests are now genuinely hermetic.
- Sleep-based slowness shrunk where safe (codex_ttfb_watchdog,
  compression_concurrent_fork, etc.); no wall-clock assertion tightened.

Verification: full hermetic suite via scripts/run_tests.sh —
2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall
(baseline: 583s wall, 13,564s subprocess CPU).
2026-07-29 13:10:23 -07:00

232 lines
7.8 KiB
Python

import json
from unittest.mock import AsyncMock
from gateway.config import Platform, PlatformConfig, load_gateway_config
def _make_adapter(require_mention=None, mention_patterns=None, free_response_chats=None,
dm_policy=None, allow_from=None, group_policy=None, group_allow_from=None):
from plugins.platforms.whatsapp.adapter import WhatsAppAdapter
extra = {}
if require_mention is not None:
extra["require_mention"] = require_mention
if mention_patterns is not None:
extra["mention_patterns"] = mention_patterns
if free_response_chats is not None:
extra["free_response_chats"] = free_response_chats
if dm_policy is not None:
extra["dm_policy"] = dm_policy
if allow_from is not None:
extra["allow_from"] = allow_from
if group_policy is not None:
extra["group_policy"] = group_policy
if group_allow_from is not None:
extra["group_allow_from"] = group_allow_from
adapter = object.__new__(WhatsAppAdapter)
adapter.platform = Platform.WHATSAPP
adapter.config = PlatformConfig(enabled=True, extra=extra)
adapter._message_handler = AsyncMock()
adapter._dm_policy = str(extra.get("dm_policy", "pairing")).strip().lower()
adapter._allow_from = WhatsAppAdapter._coerce_allow_list(extra.get("allow_from"))
adapter._group_policy = str(extra.get("group_policy", "pairing")).strip().lower()
adapter._group_allow_from = WhatsAppAdapter._coerce_allow_list(extra.get("group_allow_from"))
adapter._mention_patterns = adapter._compile_mention_patterns()
adapter._free_response_chats = adapter._whatsapp_free_response_chats()
return adapter
def _group_message(body="hello", **overrides):
data = {
"isGroup": True,
"body": body,
"chatId": "120363001234567890@g.us",
"mentionedIds": [],
"botIds": ["15551230000@s.whatsapp.net", "15551230000@lid"],
"quotedParticipant": "",
}
data.update(overrides)
return data
def _dm_message(body="hello", **overrides):
data = {
"isGroup": False,
"body": body,
"senderId": "6281234567890@s.whatsapp.net",
"from": "6281234567890@s.whatsapp.net",
"botIds": [],
"mentionedIds": [],
}
data.update(overrides)
return data
# --- Existing tests (unchanged logic, updated helper) ---
def test_group_messages_can_require_direct_trigger_via_config():
adapter = _make_adapter(require_mention=True, group_policy="open")
assert adapter._should_process_message(_group_message("hello everyone")) is False
assert adapter._should_process_message(
_group_message(
"hi there",
mentionedIds=["15551230000@s.whatsapp.net"],
)
) is True
assert adapter._should_process_message(
_group_message(
"replying",
quotedParticipant="15551230000@lid",
)
) is True
assert adapter._should_process_message(_group_message("/status")) is True
def test_regex_mention_patterns_allow_custom_wake_words():
adapter = _make_adapter(
require_mention=True,
mention_patterns=[r"^\s*chompy\b"],
group_policy="open",
)
assert adapter._should_process_message(_group_message("chompy status")) is True
assert adapter._should_process_message(_group_message(" chompy help")) is True
assert adapter._should_process_message(_group_message("hey chompy")) is False
def test_invalid_regex_patterns_are_ignored():
adapter = _make_adapter(
require_mention=True,
mention_patterns=[r"(", r"^\s*chompy\b"],
group_policy="open",
)
assert adapter._should_process_message(_group_message("chompy status")) is True
assert adapter._should_process_message(_group_message("hello everyone")) is False
def test_free_response_chats_bypass_mention_gating():
adapter = _make_adapter(
require_mention=True,
free_response_chats=["120363001234567890@g.us"],
group_policy="open",
)
assert adapter._should_process_message(_group_message("hello everyone")) is True
def test_free_response_chats_does_not_bypass_other_groups():
adapter = _make_adapter(
require_mention=True,
free_response_chats=["999999999999@g.us"],
group_policy="open",
)
assert adapter._should_process_message(_group_message("hello everyone")) is False
def test_mention_stripping_removes_bot_phone_from_body():
adapter = _make_adapter(require_mention=True)
data = _group_message("@15551230000 what is the weather?")
cleaned = adapter._clean_bot_mention_text(data["body"], data)
assert "15551230000" not in cleaned
assert "weather" in cleaned
# --- New dm_policy tests ---
def test_dm_policy_disabled_still_allows_groups():
adapter = _make_adapter(
dm_policy="disabled",
require_mention=False,
group_policy="open",
)
assert adapter._should_process_message(_group_message("hello")) is True
# --- New group_policy tests ---
# --- Config bridging tests ---
def test_config_bridges_whatsapp_dm_and_group_policy(monkeypatch, tmp_path):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
(hermes_home / "config.yaml").write_text(
"whatsapp:\n"
" dm_policy: disabled\n"
" group_policy: allowlist\n"
" group_allow_from:\n"
" - \"120363001234567890@g.us\"\n",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.delenv("WHATSAPP_DM_POLICY", raising=False)
monkeypatch.delenv("WHATSAPP_GROUP_POLICY", raising=False)
monkeypatch.delenv("WHATSAPP_GROUP_ALLOWED_USERS", raising=False)
config = load_gateway_config()
assert config is not None
assert config.platforms[Platform.WHATSAPP].extra["dm_policy"] == "disabled"
assert config.platforms[Platform.WHATSAPP].extra["group_policy"] == "allowlist"
assert config.platforms[Platform.WHATSAPP].extra["group_allow_from"] == ["120363001234567890@g.us"]
assert __import__("os").environ["WHATSAPP_DM_POLICY"] == "disabled"
assert __import__("os").environ["WHATSAPP_GROUP_POLICY"] == "allowlist"
assert __import__("os").environ["WHATSAPP_GROUP_ALLOWED_USERS"] == "120363001234567890@g.us"
# --- Broadcast / status / newsletter pseudo-chats are always dropped ---
def test_status_broadcast_chats_are_always_dropped():
"""Felipe's gateway.log showed the agent replying to status@broadcast
(a contact's WhatsApp Story update). These pseudo-chats aren't real
conversations and the adapter must drop them regardless of dm_policy.
"""
# Even on the most permissive config — open DMs, no allowlist — Stories
# and Channel posts must not reach the agent.
adapter = _make_adapter(dm_policy="open")
# Classic Story update — what Felipe was seeing in production.
status_msg = _dm_message(
body="[video received]",
chatId="status@broadcast",
senderId="34612345678@s.whatsapp.net",
)
assert adapter._should_process_message(status_msg) is False
# Channel / Newsletter broadcast posts.
newsletter_msg = _dm_message(
body="check out our latest post",
chatId="120363999999999999@newsletter",
senderId="120363999999999999@newsletter",
)
assert adapter._should_process_message(newsletter_msg) is False
def test_broadcast_filter_runs_before_allowlist():
"""A status@broadcast message from an allowlisted sender still drops —
we never want to reply to Stories, even from authorized contacts.
"""
adapter = _make_adapter(
dm_policy="allowlist",
allow_from=["34612345678@s.whatsapp.net"],
)
msg = _dm_message(
body="[image received]",
chatId="status@broadcast",
senderId="34612345678@s.whatsapp.net",
)
assert adapter._should_process_message(msg) is False