hermes-agent/tests/gateway/test_whatsapp_from_owner.py
Kong 24aa02179b test(whatsapp): repoint owner test import after adapter relocation
WhatsAppAdapter lives under plugins/platforms/whatsapp/adapter.py on
current upstream; the owner-forward test still imported the removed
gateway.platforms.whatsapp module.
2026-06-30 03:41:43 -07:00

121 lines
4 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
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