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.
116 lines
4.2 KiB
Python
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
|