mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-14 14:12:44 +00:00
Follow-up to the salvaged fail-closed defaults. The own-policy default flip (open -> pairing) and the email dispatch-level deny broke sibling tests across the suite that relied on the old fail-open behavior: - test_email.py: dispatch-mechanics tests now opt into EMAIL_ALLOW_ALL_USERS (they test formatting/attachments/threading, not authz); the two auth contract tests are rewritten to assert the new fail-closed behavior (no allowlist + no allow-all => sender dropped at the adapter). - test_whatsapp_cloud.py / test_whatsapp_formatting.py / test_whatsapp_from_owner.py: autouse fixture opts into WHATSAPP_ALLOW_ALL_USERS so dm_policy: open dispatch-mechanics tests still flow (open now requires an explicit allow-all opt-in, SECURITY.md 2.6). - _adapter_for_source: use getattr for source.platform/profile so bare SimpleNamespace test fixtures without .profile don't crash the busy/queue ingress path (AGENTS.md pitfall #17). Full tests/gateway/ + yuanbao pipeline: 8555 passed, 0 failed.
131 lines
4.3 KiB
Python
131 lines
4.3 KiB
Python
"""Tests for WhatsApp owner-message metadata and source-level text tagging.
|
|
|
|
The Node bridge sets ``fromOwner: true`` on inbound `fromMe` messages that
|
|
look owner-typed (linked-device send, not echoed from /send) when the
|
|
operator opts into ``WHATSAPP_FORWARD_OWNER_MESSAGES``. These tests pin
|
|
the adapter's responsibility: lift that flag onto
|
|
``MessageEvent.metadata["whatsapp_from_owner"]``, prefix ``MessageEvent.text``
|
|
with ``[owner reply] ``, and otherwise leave metadata absent and text
|
|
unchanged. The env-var gate itself lives in the bridge — the adapter just
|
|
trusts the payload.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from gateway.config import Platform, PlatformConfig
|
|
from plugins.platforms.whatsapp.adapter import WhatsAppAdapter
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _whatsapp_open_optin(monkeypatch):
|
|
"""Opt into WhatsApp allow-all so ``dm_policy: open`` dispatch tests run.
|
|
|
|
The adapter fails closed on ``open`` without an allow-all opt-in
|
|
(SECURITY.md 2.6); these owner-DM tests set ``_dm_policy = "open"``.
|
|
"""
|
|
monkeypatch.setenv("WHATSAPP_ALLOW_ALL_USERS", "true")
|
|
|
|
|
|
def _make_adapter():
|
|
adapter = WhatsAppAdapter.__new__(WhatsAppAdapter)
|
|
adapter.platform = Platform.WHATSAPP
|
|
adapter.config = PlatformConfig(enabled=True)
|
|
adapter._message_handler = AsyncMock()
|
|
adapter._dm_policy = "open"
|
|
adapter._allow_from = set()
|
|
adapter._group_policy = "open"
|
|
adapter._group_allow_from = set()
|
|
adapter._mention_patterns = []
|
|
adapter._free_response_chats = set()
|
|
adapter._whatsapp_free_response_chats = lambda: set()
|
|
return adapter
|
|
|
|
|
|
def _dm_payload(**overrides):
|
|
payload = {
|
|
"messageId": "M1",
|
|
"chatId": "6281234567890@s.whatsapp.net",
|
|
"senderId": "6281234567890@s.whatsapp.net",
|
|
"senderName": "Customer",
|
|
"chatName": "Customer",
|
|
"isGroup": False,
|
|
"body": "hi from the linked phone",
|
|
"hasMedia": False,
|
|
"mediaType": "",
|
|
"mediaUrls": [],
|
|
"mentionedIds": [],
|
|
"quotedParticipant": "",
|
|
"botIds": [],
|
|
"timestamp": 0,
|
|
}
|
|
payload.update(overrides)
|
|
return payload
|
|
|
|
|
|
def test_metadata_flag_set_when_payload_has_from_owner():
|
|
adapter = _make_adapter()
|
|
payload = _dm_payload(fromOwner=True)
|
|
|
|
event = asyncio.run(adapter._build_message_event(payload))
|
|
|
|
assert event is not None
|
|
assert event.metadata.get("whatsapp_from_owner") is True
|
|
assert event.text.startswith("[owner reply] ")
|
|
assert event.text == "[owner reply] hi from the linked phone"
|
|
|
|
|
|
def test_from_owner_does_not_double_prefix_when_already_tagged():
|
|
adapter = _make_adapter()
|
|
payload = _dm_payload(
|
|
fromOwner=True,
|
|
body="[owner reply] already tagged",
|
|
)
|
|
|
|
event = asyncio.run(adapter._build_message_event(payload))
|
|
|
|
assert event is not None
|
|
assert event.metadata.get("whatsapp_from_owner") is True
|
|
assert event.text == "[owner reply] already tagged"
|
|
|
|
|
|
def test_from_owner_prefixes_empty_body_for_uniform_media_placeholders():
|
|
"""Owner media with empty caption still gets the marker (bridge may
|
|
substitute placeholders like ``[image received]`` upstream; empty stays
|
|
tagged for consistency)."""
|
|
adapter = _make_adapter()
|
|
payload = _dm_payload(fromOwner=True, body="")
|
|
|
|
event = asyncio.run(adapter._build_message_event(payload))
|
|
|
|
assert event is not None
|
|
assert event.metadata.get("whatsapp_from_owner") is True
|
|
assert event.text == "[owner reply] "
|
|
|
|
|
|
def test_metadata_flag_absent_by_default():
|
|
"""Default bridge payload (env flag off → field never present) must not
|
|
leak the metadata key. Plugins use ``.get(...)`` and rely on absence."""
|
|
adapter = _make_adapter()
|
|
payload = _dm_payload()
|
|
|
|
event = asyncio.run(adapter._build_message_event(payload))
|
|
|
|
assert event is not None
|
|
assert "whatsapp_from_owner" not in event.metadata
|
|
|
|
|
|
def test_metadata_flag_absent_when_explicitly_false():
|
|
"""Explicit fromOwner=false must not set the metadata key — plugins
|
|
test for truthiness, but absence is the canonical "not owner" state."""
|
|
adapter = _make_adapter()
|
|
payload = _dm_payload(fromOwner=False)
|
|
|
|
event = asyncio.run(adapter._build_message_event(payload))
|
|
|
|
assert event is not None
|
|
assert "whatsapp_from_owner" not in event.metadata
|