Files
hermes-agent/tests/gateway/test_whatsapp_allowlist_lid_resolution.py
Teknium 39975613b1 test: prune wave 2 + speed fixes — 28,106 → 19,757 test functions, suite wall 315s → 294s
Second, deeper pass over tools/gateway/hermes_cli plus first pass over
the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker,
dashboard, conformance, monitoring, secret_sources, hermes_state,
providers). Same rubric as wave 1 (AGENTS.md test policy); security,
alternation/caching invariants, issue-number regressions, and E2E kept.

Real test-quality fixes found and rooted out along the way:
- tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls
  (DEFAULT_CONFIG smart-approval leaked in) — pinned approval
  mode=manual via autouse fixture: 17.4s → 0.4s.
- test_model_switch_custom_providers.py / test_user_providers_model_switch.py
  silently probed live provider catalogs (~2s/test) — stubbed
  cached_provider_model_ids/provider_model_ids/fetch_api_models.
- test_telegram_noise_filter.py: 15-platform copy-paste matrix over
  shared gateway.run logic → 3 representative platforms (55s → 3.9s).
- test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on
  MagicMock agents — interrupt.side_effect now clears _running_agents
  (22s → 1.0s).
- test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x
  (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps
  patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait
  5s → 0.5s.
- test_telegram_init_deadline.py: loop-block margin restored to 1.0s
  with rationale comment — the watchdog-dump assertion needs the loop
  blocked well past deadline+grace under parallel load (flaked once in
  the 40-worker verification run at a 0.2s margin).

Verification: full hermetic suite via scripts/run_tests.sh —
2,438 files, 21,718 tests passed, 0 failed, 293.9s wall.
Suite totals vs original baseline: 46,820 → 19,757 test functions
(−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
2026-07-29 13:39:40 -07:00

116 lines
4.2 KiB
Python

"""WhatsApp DM/group allowlist must resolve phone↔LID aliases at intake.
Regression for #14486: WhatsApp now delivers inbound DM senders in LID form
(``<id>@lid``) while operators configure the allowlist with phone numbers.
The adapter-level gate (``_is_dm_allowed`` / ``_is_group_allowed`` →
``_should_process_message``) did a raw set-membership check with no LID
resolution, so every DM from an allowed user was silently dropped before the
gateway authz layer ever ran.
The fix routes the adapter gate through the shared
``gateway.whatsapp_identity.expand_whatsapp_aliases`` helper, which reads the
bridge's ``lid-mapping-*.json`` session files (the same source the gateway
authz and session-key paths already use).
"""
import json
from unittest.mock import AsyncMock
from gateway.config import Platform, PlatformConfig
from hermes_constants import get_hermes_home
PHONE = "351912345678"
LID = "77214955630717"
def _make_adapter(dm_policy=None, allow_from=None, group_policy=None, group_allow_from=None):
from plugins.platforms.whatsapp.adapter import WhatsAppAdapter
extra = {}
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", "open")).strip().lower()
adapter._allow_from = WhatsAppAdapter._coerce_allow_list(extra.get("allow_from"))
adapter._group_policy = str(extra.get("group_policy", "open")).strip().lower()
adapter._group_allow_from = WhatsAppAdapter._coerce_allow_list(
extra.get("group_allow_from")
)
return adapter
def _write_lid_mapping(phone=PHONE, lid=LID):
"""Mirror what the JS bridge writes: phone→lid and lid→phone (reverse)."""
session_dir = get_hermes_home() / "whatsapp" / "session"
session_dir.mkdir(parents=True, exist_ok=True)
(session_dir / f"lid-mapping-{phone}.json").write_text(json.dumps(lid), encoding="utf-8")
(session_dir / f"lid-mapping-{lid}_reverse.json").write_text(
json.dumps(phone), encoding="utf-8"
)
# --------------------------------------------------------------------- DM gate
def test_dm_phone_allowlist_matches_lid_sender():
"""allow_from has the phone number; inbound sender arrives as @lid (the bug)."""
_write_lid_mapping()
adapter = _make_adapter(dm_policy="allowlist", allow_from=[PHONE])
assert adapter._is_dm_allowed(f"{LID}@lid") is True
def test_dm_phone_with_plus_allowlist_matches_lid_sender():
"""A ``+``-prefixed phone allowlist entry still resolves to the LID sender."""
_write_lid_mapping()
adapter = _make_adapter(dm_policy="allowlist", allow_from=[f"+{PHONE}"])
assert adapter._is_dm_allowed(f"{LID}@lid") is True
# ------------------------------------------------------------------ group gate
def test_group_jid_exact_match_still_works():
"""Group allowlists use full ``@g.us`` JIDs — exact match must pass through."""
adapter = _make_adapter(
group_policy="allowlist", group_allow_from=["120363001234567890@g.us"]
)
assert adapter._is_group_allowed("120363001234567890@g.us") is True
def test_group_unlisted_jid_blocked():
adapter = _make_adapter(
group_policy="allowlist", group_allow_from=["120363001234567890@g.us"]
)
assert adapter._is_group_allowed("120363009999999999@g.us") is False
# ------------------------------------------------------ end-to-end intake gate
def test_should_process_message_dm_phone_allowlist_lid_sender():
"""Full intake path: a DM from a phone-allowlisted contact arriving as @lid."""
_write_lid_mapping()
adapter = _make_adapter(dm_policy="allowlist", allow_from=[PHONE])
data = {
"isGroup": False,
"body": "hello",
"senderId": f"{LID}@lid",
"from": f"{LID}@lid",
"botIds": [],
"mentionedIds": [],
}
assert adapter._should_process_message(data) is True