ab2f4602de
Breaks the two import cycles that forced Protocol stand-ins in the F821 sweep, so the two sites now name the real types. gateway/platforms/event.py (new leaf): MessageType, ProcessingOutcome, MessageEvent moved out of base.py verbatim. Their only dependency is gateway.session.SessionSource; base.py imported helpers.py at module level, so helpers could not name MessageEvent. Now TextBatchAggregator is typed by the real MessageEvent. 249 importers repointed (`from gateway.platforms.base import` -> `.event`, preserving each import's layout); gateway.platforms.__init__ re-exports from .event. The three revert-scheduled PLUGIN-COMPAT pointers that named these symbols (gateway.slash_commands → MessageType, dingtalk → MessageType, photon → ProcessingOutcome) and their COMPAT_MANIFEST rows now target gateway.platforms.event. Docs updated: ADDING_A_PLATFORM.md, adding-platform-adapters.md (en + zh-Hans). tools/mcp_tool_sampling.py: ElicitationHandler no longer holds a back-reference to its MCPServerTask (mcp_tool imports sampling, so the task type cannot be named there). It only ever read owner._pending_call_context, so it takes `call_context: Callable[[], Context | None]` and MCPServerTask passes `lambda: self._pending_call_context`. The consent call is one `functools.partial`, run directly or inside the captured Context. ty on the 11 touched production files vs origin/main: 0 new diagnostics, 14 resolved. (The one `source: SessionSource = None` diagnostic moves with the class; typing it Optional exposes ~60 unguarded call sites — separate follow-up.) Tests: tests/gateway + tests/plugins + tests/tools + touched files, 18,235 passed; the 31 failures reproduce identically on origin/main (macOS /private/tmp, systemd socket, long-path fixtures, live-service tests).
123 lines
4.6 KiB
Python
123 lines
4.6 KiB
Python
"""Tests for WhatsApp reply_prefix config.yaml support.
|
|
|
|
Covers:
|
|
- config.yaml whatsapp.reply_prefix bridging into PlatformConfig.extra
|
|
- WhatsAppAdapter reading reply_prefix from config.extra
|
|
- Bridge subprocess receiving WHATSAPP_REPLY_PREFIX env var
|
|
- config.yaml whatsapp.send_read_receipts bridging into PlatformConfig.extra
|
|
- WhatsAppAdapter parsing send_read_receipts as a boolean
|
|
- Config version covers all ENV_VARS_BY_VERSION keys (regression guard)
|
|
"""
|
|
|
|
import asyncio
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from gateway.config import Platform, PlatformConfig
|
|
from gateway.platforms.event import MessageEvent, MessageType
|
|
|
|
|
|
class _AsyncResponseContext:
|
|
def __init__(self, response):
|
|
self.response = response
|
|
|
|
async def __aenter__(self):
|
|
return self.response
|
|
|
|
async def __aexit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Config bridging from config.yaml
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestConfigYamlBridging:
|
|
"""Test that whatsapp.reply_prefix in config.yaml flows into PlatformConfig."""
|
|
|
|
def test_reply_prefix_bridged_from_yaml(self, tmp_path):
|
|
"""whatsapp.reply_prefix in config.yaml sets PlatformConfig.extra."""
|
|
config_yaml = tmp_path / "config.yaml"
|
|
config_yaml.write_text('whatsapp:\n reply_prefix: "Custom Bot"\n')
|
|
|
|
with patch("gateway.config.get_hermes_home", return_value=tmp_path):
|
|
from gateway.config import load_gateway_config
|
|
# Need to also patch WHATSAPP_ENABLED so the platform exists
|
|
with patch.dict("os.environ", {"WHATSAPP_ENABLED": "true"}, clear=False):
|
|
config = load_gateway_config()
|
|
|
|
wa_config = config.platforms.get(Platform.WHATSAPP)
|
|
assert wa_config is not None
|
|
assert wa_config.extra.get("reply_prefix") == "Custom Bot"
|
|
|
|
def test_empty_reply_prefix_bridged(self, tmp_path):
|
|
"""Empty string reply_prefix disables the header."""
|
|
config_yaml = tmp_path / "config.yaml"
|
|
config_yaml.write_text('whatsapp:\n reply_prefix: ""\n')
|
|
|
|
with patch("gateway.config.get_hermes_home", return_value=tmp_path):
|
|
from gateway.config import load_gateway_config
|
|
with patch.dict("os.environ", {"WHATSAPP_ENABLED": "true"}, clear=False):
|
|
config = load_gateway_config()
|
|
|
|
wa_config = config.platforms.get(Platform.WHATSAPP)
|
|
assert wa_config is not None
|
|
assert wa_config.extra.get("reply_prefix") == ""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# WhatsAppAdapter __init__
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestAdapterInit:
|
|
"""Test that WhatsAppAdapter reads reply_prefix from config.extra."""
|
|
|
|
def test_reply_prefix_from_extra(self):
|
|
from plugins.platforms.whatsapp.adapter import WhatsAppAdapter
|
|
config = PlatformConfig(enabled=True, extra={"reply_prefix": "Bot\\n"})
|
|
adapter = WhatsAppAdapter(config)
|
|
assert adapter._reply_prefix == "Bot\\n"
|
|
|
|
|
|
class TestReadReceiptPolicyOrdering:
|
|
@pytest.mark.asyncio
|
|
async def test_accepted_receipt_key_is_sent_to_bridge(self):
|
|
from plugins.platforms.whatsapp.adapter import WhatsAppAdapter
|
|
|
|
adapter = WhatsAppAdapter(
|
|
PlatformConfig(enabled=True, extra={"send_read_receipts": True})
|
|
)
|
|
response = SimpleNamespace(status=200)
|
|
session = MagicMock()
|
|
session.post.return_value = _AsyncResponseContext(response)
|
|
adapter._http_session = session
|
|
key = {
|
|
"id": "incoming-1",
|
|
"remoteJid": "120363001234567890@g.us",
|
|
"participant": "15550001111@s.whatsapp.net",
|
|
"fromMe": False,
|
|
}
|
|
|
|
await adapter._send_read_receipt({"readReceiptKey": key})
|
|
|
|
assert session.post.call_args.kwargs["json"] == {"key": key}
|
|
assert session.post.call_args.args[0].endswith("/read")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Config version regression guard
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestConfigVersionCoverage:
|
|
"""Ensure _config_version covers all ENV_VARS_BY_VERSION keys."""
|
|
|
|
def test_default_config_version_covers_env_var_versions(self):
|
|
"""_config_version must be >= the highest ENV_VARS_BY_VERSION key."""
|
|
from hermes_cli.config import DEFAULT_CONFIG, ENV_VARS_BY_VERSION
|
|
assert DEFAULT_CONFIG["_config_version"] >= max(ENV_VARS_BY_VERSION)
|