fix(whatsapp-cloud): review follow-ups for #43921

- nous_subscription: gate the STT managed-default flip on openai-audio
  entitlement and skip when a local backend (faster-whisper or custom
  command) works; new _local_stt_backend_available() helper + tests
- whatsapp_cloud: WHATSAPP_CLOUD_{DM_POLICY,ALLOW_FROM,GROUP_POLICY,
  GROUP_ALLOW_FROM} env overrides so both adapters can run in parallel;
  normalize allowlist entries (JID/punctuation) to bare wa_id
- whatsapp_cloud: wrap per-message event build in try/except (dedup-marked
  wamids would be silently dropped on Meta's batch retry otherwise)
- whatsapp_cloud: validate media_id before URL/filename interpolation,
  delete transient .ogg after voice upload, FIFO-cap interactive-button
  state dicts and per-chat wamid cache
- whatsapp_common: '# **Title**' headers no longer double-wrap asterisks
- setup wizard: read access token / app secret via getpass on TTYs
- docs: new WHATSAPP_CLOUD_* gating env vars
This commit is contained in:
teknium1
2026-06-11 07:51:01 -07:00
parent 2ecb4e62bb
commit 52c7976f40
8 changed files with 319 additions and 35 deletions
+71
View File
@@ -2248,3 +2248,74 @@ class TestSendTyping:
await adapter.send_typing("15551234567")
headers = adapter._http_client.post.call_args.kwargs["headers"]
assert headers["Authorization"] == "Bearer my-test-token"
# ---------------------------------------------------------------------------
# Allowlist normalization + env decoupling (salvage follow-up)
# ---------------------------------------------------------------------------
class TestAllowlistNormalization:
def test_normalize_allow_ids_strips_jid_suffix_and_punctuation(self):
from gateway.platforms.whatsapp_cloud import WhatsAppCloudAdapter
ids = {"15551234567@s.whatsapp.net", "+1 (555) 765-4321", "15550000000"}
normalized = WhatsAppCloudAdapter._normalize_allow_ids(ids)
assert normalized == {"15551234567", "15557654321", "15550000000"}
def test_dm_allowlist_matches_bare_wa_id_against_jid_entry(self):
"""A Baileys-style JID in the allowlist must match the Cloud API's
bare wa_id sender — users share allowlists between both adapters."""
from gateway.platforms.whatsapp_cloud import WhatsAppCloudAdapter
adapter = _make_adapter()
adapter._dm_policy = "allowlist"
adapter._allow_from = WhatsAppCloudAdapter._normalize_allow_ids(
{"15551234567@s.whatsapp.net"}
)
assert adapter._is_dm_allowed("15551234567") is True
assert adapter._is_dm_allowed("19998887777") is False
def test_cloud_env_overrides_take_precedence(self, monkeypatch):
"""WHATSAPP_CLOUD_DM_POLICY wins over the shared WHATSAPP_DM_POLICY
so both adapters can run in parallel with independent policies."""
from gateway.platforms.whatsapp_cloud import WhatsAppCloudAdapter
monkeypatch.setenv("WHATSAPP_DM_POLICY", "allowlist")
monkeypatch.setenv("WHATSAPP_CLOUD_DM_POLICY", "open")
monkeypatch.setenv("WHATSAPP_CLOUD_ALLOW_FROM", "+1 555 123 4567")
config = MagicMock()
config.extra = {
"phone_number_id": "123",
"access_token": "tok",
}
adapter = WhatsAppCloudAdapter(config)
assert adapter._dm_policy == "open"
assert adapter._allow_from == {"15551234567"}
class TestBoundedInteractiveState:
def test_bounded_put_evicts_oldest(self):
from collections import OrderedDict
from gateway.platforms.whatsapp_cloud import (
INTERACTIVE_STATE_CACHE_SIZE,
WhatsAppCloudAdapter,
)
cache: OrderedDict = OrderedDict()
for i in range(INTERACTIVE_STATE_CACHE_SIZE + 10):
WhatsAppCloudAdapter._bounded_put(cache, f"id-{i}", "sess")
assert len(cache) == INTERACTIVE_STATE_CACHE_SIZE
assert "id-0" not in cache
assert f"id-{INTERACTIVE_STATE_CACHE_SIZE + 9}" in cache
class TestMediaIdValidation:
@pytest.mark.asyncio
async def test_traversal_media_id_refused(self):
adapter = _make_adapter()
adapter._http_client = MagicMock() # would be used if not refused
path, mime = await adapter._download_media_to_cache("../../etc/passwd")
assert path is None and mime is None
adapter._http_client.get.assert_not_called()
@@ -91,6 +91,13 @@ class TestFormatMessage:
assert adapter.format_message("## Subtitle") == "*Subtitle*"
assert adapter.format_message("### Deep") == "*Deep*"
def test_bold_header_does_not_double_wrap(self):
""""# **Title**" must become *Title*, not **Title** (WhatsApp would
render the doubled asterisks literally)."""
adapter = _make_adapter()
assert adapter.format_message("# **Title**") == "*Title*"
assert adapter.format_message("## __Strong__") == "*Strong*"
def test_links_converted(self):
adapter = _make_adapter()
result = adapter.format_message("[click here](https://example.com)")