From f080fedf13f057058dcce3a20d48a1fe4edcbf1c Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sun, 7 Jun 2026 05:27:58 -0700 Subject: [PATCH] test+fix: rewrite migrated-platform test imports; fix connected-check discovery, telegram extra precedence, prompt_choice import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sweep test imports gateway.platforms.{telegram,feishu,matrix,dingtalk,whatsapp} (+ telegram_network / feishu satellites) -> plugins.platforms..adapter across ~68 test files; both 'from X import' and 'from gateway.platforms import X' forms. - _is_platform_connected now forces discover_plugins() before the registry lookup so get_connected_platforms() works on directly-constructed GatewayConfig (fixes dingtalk-recognised tests after its checker moved to the plugin). - telegram _apply_yaml_config no longer re-emits generic shared-config keys (reply_prefix etc.) that _merge_platform_map already merges with top-level precedence, while still passing through telegram-specific extras (base_url). - prompt_choice lives in hermes_cli.setup, not cli_output — fixed the import in feishu/dingtalk interactive_setup and the test patch target. - test_setup_feishu rewritten to drive interactive_setup via the plugin. --- gateway/config.py | 10 +- plugins/platforms/dingtalk/adapter.py | 2 +- plugins/platforms/feishu/adapter.py | 2 +- plugins/platforms/telegram/adapter.py | 15 +- tests/e2e/conftest.py | 2 +- tests/gateway/conftest.py | 2 +- tests/gateway/feishu_helpers.py | 2 +- .../gateway/test_allowed_channels_widening.py | 6 +- tests/gateway/test_config.py | 2 +- .../test_config_driven_access_policy.py | 2 +- tests/gateway/test_dingtalk.py | 108 ++-- tests/gateway/test_dm_topics.py | 4 +- tests/gateway/test_feishu.py | 482 +++++++++--------- tests/gateway/test_feishu_approval_buttons.py | 4 +- tests/gateway/test_feishu_bot_admission.py | 32 +- tests/gateway/test_feishu_comment.py | 90 ++-- tests/gateway/test_feishu_comment_rules.py | 14 +- tests/gateway/test_feishu_meeting_invite.py | 4 +- tests/gateway/test_feishu_onboard.py | 156 +++--- tests/gateway/test_matrix.py | 108 ++-- ...st_matrix_approval_reaction_fail_closed.py | 4 +- tests/gateway/test_matrix_exec_approval.py | 4 +- tests/gateway/test_matrix_mention.py | 2 +- tests/gateway/test_matrix_voice.py | 2 +- .../test_platform_http_client_limits.py | 4 +- tests/gateway/test_send_image_file.py | 2 +- tests/gateway/test_send_multiple_images.py | 2 +- tests/gateway/test_setup_feishu.py | 36 +- tests/gateway/test_stream_consumer.py | 10 +- .../test_stream_consumer_fresh_final.py | 2 +- .../test_stream_consumer_thread_routing.py | 4 +- .../gateway/test_telegram_approval_buttons.py | 2 +- ...test_telegram_callback_auth_fail_closed.py | 2 +- tests/gateway/test_telegram_caption_merge.py | 2 +- tests/gateway/test_telegram_channel_posts.py | 2 +- .../gateway/test_telegram_clarify_buttons.py | 2 +- tests/gateway/test_telegram_conflict.py | 16 +- tests/gateway/test_telegram_documents.py | 12 +- tests/gateway/test_telegram_format.py | 2 +- tests/gateway/test_telegram_forum_commands.py | 2 +- tests/gateway/test_telegram_group_gating.py | 2 +- tests/gateway/test_telegram_max_doc_bytes.py | 2 +- .../test_telegram_mention_boundaries.py | 2 +- tests/gateway/test_telegram_model_picker.py | 4 +- tests/gateway/test_telegram_network.py | 6 +- .../test_telegram_network_reconnect.py | 6 +- tests/gateway/test_telegram_reactions.py | 2 +- tests/gateway/test_telegram_reply_mode.py | 2 +- tests/gateway/test_telegram_reply_quote.py | 2 +- .../test_telegram_send_draft_format.py | 4 +- .../gateway/test_telegram_send_path_health.py | 6 +- tests/gateway/test_telegram_slash_confirm.py | 2 +- tests/gateway/test_telegram_status_update.py | 2 +- .../gateway/test_telegram_text_batch_perf.py | 2 +- tests/gateway/test_telegram_text_batching.py | 2 +- .../gateway/test_telegram_thread_fallback.py | 8 +- tests/gateway/test_text_batching.py | 6 +- tests/gateway/test_whatsapp_connect.py | 62 +-- tests/gateway/test_whatsapp_formatting.py | 4 +- tests/gateway/test_whatsapp_group_gating.py | 4 +- tests/gateway/test_whatsapp_reply_prefix.py | 6 +- tests/gateway/test_whatsapp_text_batching.py | 2 +- tests/gateway/test_ws_auth_retry.py | 6 +- tests/hermes_cli/test_logs.py | 6 +- tests/test_hermes_logging.py | 4 +- tests/tools/test_send_message_tool.py | 2 +- 66 files changed, 669 insertions(+), 648 deletions(-) diff --git a/gateway/config.py b/gateway/config.py index 01184fd271c1..c905d77775d9 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -532,9 +532,17 @@ class GatewayConfig: if checker is not None: return checker(config) - # Plugin-registered platforms + # Plugin-registered platforms. Force plugin discovery first so this + # works even when GatewayConfig is constructed directly (e.g. in tests + # or callers that bypass load_gateway_config(), which is what triggers + # discovery in the normal path). discover_plugins() is idempotent. try: from gateway.platform_registry import platform_registry + try: + from hermes_cli.plugins import discover_plugins + discover_plugins() + except Exception: + pass entry = platform_registry.get(platform.value) if entry: if entry.is_connected is not None: diff --git a/plugins/platforms/dingtalk/adapter.py b/plugins/platforms/dingtalk/adapter.py index 30da3c3f0d2f..528cb38c1fd2 100644 --- a/plugins/platforms/dingtalk/adapter.py +++ b/plugins/platforms/dingtalk/adapter.py @@ -1565,9 +1565,9 @@ def interactive_setup() -> None: lazy-imported so the plugin's module-load surface stays minimal. """ from hermes_cli.config import get_env_value, save_env_value + from hermes_cli.setup import prompt_choice from hermes_cli.cli_output import ( prompt, - prompt_choice, prompt_yes_no, print_header, print_success, diff --git a/plugins/platforms/feishu/adapter.py b/plugins/platforms/feishu/adapter.py index f3abd77f1091..c63d58c15365 100644 --- a/plugins/platforms/feishu/adapter.py +++ b/plugins/platforms/feishu/adapter.py @@ -5300,9 +5300,9 @@ def interactive_setup() -> None: _PLATFORMS["feishu"] dict. CLI helpers are lazy-imported. """ from hermes_cli.config import get_env_value, save_env_value + from hermes_cli.setup import prompt_choice from hermes_cli.cli_output import ( prompt, - prompt_choice, prompt_yes_no, print_header, print_info, diff --git a/plugins/platforms/telegram/adapter.py b/plugins/platforms/telegram/adapter.py index 408097b80355..1fed0baabb3d 100644 --- a/plugins/platforms/telegram/adapter.py +++ b/plugins/platforms/telegram/adapter.py @@ -6252,8 +6252,21 @@ def _apply_yaml_config(yaml_cfg: dict, telegram_cfg: dict) -> dict | None: for _key in ("guest_mode", "disable_link_previews", "observe_unmentioned_group_messages"): if _key in telegram_cfg: extras.setdefault(_key, telegram_cfg[_key]) + # Pass through telegram-specific extra keys (e.g. base_url proxy override), + # but EXCLUDE the generic shared-config keys that _merge_platform_map in + # gateway/config.py already merges with correct top-level-over-nested + # precedence. The apply_yaml_config_fn dispatch merges our return via + # dict.update() (clobber), so re-emitting those generic keys here would + # undo that precedence (top-level losing to a nested-fallback block). + _GENERIC_MERGE_KEYS = { + "reply_prefix", "reply_in_thread", "reply_to_mode", + "unauthorized_dm_behavior", "notice_delivery", "require_mention", + "channel_skill_bindings", "channel_prompts", "gateway_restart_notification", + "allow_from", "allow_admin_from", "dm_policy", "group_policy", + } for _k, _v in _telegram_extra.items(): - extras.setdefault(_k, _v) + if _k not in _GENERIC_MERGE_KEYS: + extras.setdefault(_k, _v) return extras or None diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 3adbd557dd11..7963a14d8964 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -118,7 +118,7 @@ _ensure_discord_mock() _ensure_slack_mock() import discord # noqa: E402 — mocked above -from gateway.platforms.telegram import TelegramAdapter # noqa: E402 +from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402 from plugins.platforms.discord.adapter import DiscordAdapter # noqa: E402 import gateway.platforms.slack as _slack_mod # noqa: E402 diff --git a/tests/gateway/conftest.py b/tests/gateway/conftest.py index 2d56c7c11f4c..a16eb76a6fe1 100644 --- a/tests/gateway/conftest.py +++ b/tests/gateway/conftest.py @@ -2,7 +2,7 @@ The ``_ensure_telegram_mock`` helper guarantees that a minimal mock of the ``telegram`` package is registered in :data:`sys.modules` **before** -any test file triggers ``from gateway.platforms.telegram import ...``. +any test file triggers ``from plugins.platforms.telegram.adapter import ...``. Without this, ``pytest-xdist`` workers that happen to collect ``test_telegram_caption_merge.py`` (bare top-level import, no per-file diff --git a/tests/gateway/feishu_helpers.py b/tests/gateway/feishu_helpers.py index 753a61a70a86..ae8a4bfc3718 100644 --- a/tests/gateway/feishu_helpers.py +++ b/tests/gateway/feishu_helpers.py @@ -35,7 +35,7 @@ def make_adapter_skeleton( require_mention: bool = True, group_policy: str = "allowlist", ) -> Any: - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = object.__new__(FeishuAdapter) adapter._bot_open_id = bot_open_id diff --git a/tests/gateway/test_allowed_channels_widening.py b/tests/gateway/test_allowed_channels_widening.py index 0d214713a1cc..26c1b83983de 100644 --- a/tests/gateway/test_allowed_channels_widening.py +++ b/tests/gateway/test_allowed_channels_widening.py @@ -24,7 +24,7 @@ from gateway.config import Platform, PlatformConfig # --------------------------------------------------------------------------- def _make_telegram_adapter(*, allowed_chats=None, require_mention=None, guest_mode=False): - from gateway.platforms.telegram import TelegramAdapter + from plugins.platforms.telegram.adapter import TelegramAdapter extra = {"guest_mode": guest_mode} if allowed_chats is not None: @@ -162,8 +162,8 @@ class TestTelegramAllowedChats: def _make_dingtalk_adapter(*, allowed_chats=None, require_mention=None): # Import lazily — DingTalk SDK may not be installed. - pytest.importorskip("gateway.platforms.dingtalk", reason="DingTalk adapter not importable") - from gateway.platforms.dingtalk import DingTalkAdapter + pytest.importorskip("plugins.platforms.dingtalk.adapter", reason="DingTalk adapter not importable") + from plugins.platforms.dingtalk.adapter import DingTalkAdapter extra = {} if allowed_chats is not None: diff --git a/tests/gateway/test_config.py b/tests/gateway/test_config.py index 4f2756dba3b1..81e8b3aeb76d 100644 --- a/tests/gateway/test_config.py +++ b/tests/gateway/test_config.py @@ -535,7 +535,7 @@ class TestLoadGatewayConfig: telegram = config.platforms[Platform.TELEGRAM] assert telegram.extra.get("allow_from") == ["777888999"], ( - "allow_from configured under gateway.platforms.telegram must be " + "allow_from configured under plugins.platforms.telegram.adapter must be " "bridged into PlatformConfig.extra by the shared-key loop" ) assert telegram.extra.get("require_mention") is False diff --git a/tests/gateway/test_config_driven_access_policy.py b/tests/gateway/test_config_driven_access_policy.py index fee79d90b7d0..61837701030f 100644 --- a/tests/gateway/test_config_driven_access_policy.py +++ b/tests/gateway/test_config_driven_access_policy.py @@ -107,7 +107,7 @@ def test_base_adapter_defaults_to_not_owning_access_policy(): ("gateway.platforms.weixin", "WeixinAdapter"), ("gateway.platforms.yuanbao", "YuanbaoAdapter"), ("gateway.platforms.qqbot.adapter", "QQAdapter"), - ("gateway.platforms.whatsapp", "WhatsAppAdapter"), + ("plugins.platforms.whatsapp.adapter", "WhatsAppAdapter"), ], ) def test_own_policy_adapters_declare_the_flag(module_path, class_name): diff --git a/tests/gateway/test_dingtalk.py b/tests/gateway/test_dingtalk.py index d73b687d7acc..8e4cd8223276 100644 --- a/tests/gateway/test_dingtalk.py +++ b/tests/gateway/test_dingtalk.py @@ -39,7 +39,7 @@ class _FakeChatbotMessage(SimpleNamespace): @pytest.fixture(autouse=True) def _fake_dingtalk_optional_sdks(monkeypatch): """Keep DingTalk adapter tests hermetic when optional SDKs are absent.""" - from gateway.platforms import dingtalk as dt + import plugins.platforms.dingtalk.adapter as dt card_models = SimpleNamespace(**{ name: _FakeDingTalkModel @@ -94,29 +94,29 @@ class TestDingTalkRequirements: with patch.dict("sys.modules", {"dingtalk_stream": None}), \ patch("tools.lazy_deps.ensure", side_effect=ImportError("dingtalk_stream unavailable")): monkeypatch.setattr( - "gateway.platforms.dingtalk.DINGTALK_STREAM_AVAILABLE", False + "plugins.platforms.dingtalk.adapter.DINGTALK_STREAM_AVAILABLE", False ) - from gateway.platforms.dingtalk import check_dingtalk_requirements + from plugins.platforms.dingtalk.adapter import check_dingtalk_requirements assert check_dingtalk_requirements() is False def test_returns_false_when_env_vars_missing(self, monkeypatch): monkeypatch.setattr( - "gateway.platforms.dingtalk.DINGTALK_STREAM_AVAILABLE", True + "plugins.platforms.dingtalk.adapter.DINGTALK_STREAM_AVAILABLE", True ) - monkeypatch.setattr("gateway.platforms.dingtalk.HTTPX_AVAILABLE", True) + monkeypatch.setattr("plugins.platforms.dingtalk.adapter.HTTPX_AVAILABLE", True) monkeypatch.delenv("DINGTALK_CLIENT_ID", raising=False) monkeypatch.delenv("DINGTALK_CLIENT_SECRET", raising=False) - from gateway.platforms.dingtalk import check_dingtalk_requirements + from plugins.platforms.dingtalk.adapter import check_dingtalk_requirements assert check_dingtalk_requirements() is False def test_returns_true_when_all_available(self, monkeypatch): monkeypatch.setattr( - "gateway.platforms.dingtalk.DINGTALK_STREAM_AVAILABLE", True + "plugins.platforms.dingtalk.adapter.DINGTALK_STREAM_AVAILABLE", True ) - monkeypatch.setattr("gateway.platforms.dingtalk.HTTPX_AVAILABLE", True) + monkeypatch.setattr("plugins.platforms.dingtalk.adapter.HTTPX_AVAILABLE", True) monkeypatch.setenv("DINGTALK_CLIENT_ID", "test-id") monkeypatch.setenv("DINGTALK_CLIENT_SECRET", "test-secret") - from gateway.platforms.dingtalk import check_dingtalk_requirements + from plugins.platforms.dingtalk.adapter import check_dingtalk_requirements assert check_dingtalk_requirements() is True @@ -128,7 +128,7 @@ class TestDingTalkRequirements: class TestDingTalkAdapterInit: def test_reads_config_from_extra(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter config = PlatformConfig( enabled=True, extra={"client_id": "cfg-id", "client_secret": "cfg-secret"}, @@ -141,7 +141,7 @@ class TestDingTalkAdapterInit: def test_falls_back_to_env_vars(self, monkeypatch): monkeypatch.setenv("DINGTALK_CLIENT_ID", "env-id") monkeypatch.setenv("DINGTALK_CLIENT_SECRET", "env-secret") - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter config = PlatformConfig(enabled=True) adapter = DingTalkAdapter(config) assert adapter._client_id == "env-id" @@ -156,28 +156,28 @@ class TestDingTalkAdapterInit: class TestExtractText: def test_extracts_dict_text(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter msg = MagicMock() msg.text = {"content": " hello world "} msg.rich_text = None assert DingTalkAdapter._extract_text(msg) == "hello world" def test_extracts_string_text(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter msg = MagicMock() msg.text = "plain text" msg.rich_text = None assert DingTalkAdapter._extract_text(msg) == "plain text" def test_falls_back_to_rich_text(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter msg = MagicMock() msg.text = "" msg.rich_text = [{"text": "part1"}, {"text": "part2"}, {"image": "url"}] assert DingTalkAdapter._extract_text(msg) == "part1 part2" def test_returns_empty_for_no_content(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter msg = MagicMock() msg.text = "" msg.rich_text = None @@ -192,24 +192,24 @@ class TestExtractText: class TestDeduplication: def test_first_message_not_duplicate(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) assert adapter._dedup.is_duplicate("msg-1") is False def test_second_same_message_is_duplicate(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) adapter._dedup.is_duplicate("msg-1") assert adapter._dedup.is_duplicate("msg-1") is True def test_different_messages_not_duplicate(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) adapter._dedup.is_duplicate("msg-1") assert adapter._dedup.is_duplicate("msg-2") is False def test_cache_cleanup_on_overflow(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) max_size = adapter._dedup._max_size # Fill beyond max @@ -228,7 +228,7 @@ class TestSend: @pytest.mark.asyncio async def test_send_posts_to_webhook(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) mock_response = MagicMock() @@ -254,7 +254,7 @@ class TestSend: @pytest.mark.asyncio async def test_send_fails_without_webhook(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) adapter._http_client = AsyncMock() @@ -264,7 +264,7 @@ class TestSend: @pytest.mark.asyncio async def test_send_uses_cached_webhook(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) mock_response = MagicMock() @@ -280,7 +280,7 @@ class TestSend: @pytest.mark.asyncio async def test_send_handles_http_error(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) mock_response = MagicMock() @@ -299,7 +299,7 @@ class TestSend: @pytest.mark.asyncio async def test_send_image_renders_markdown_image(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) mock_response = MagicMock() @@ -324,7 +324,7 @@ class TestSend: @pytest.mark.asyncio async def test_send_image_file_returns_explicit_unsupported_error(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) result = await adapter.send_image_file("chat-123", "/tmp/demo.png") @@ -334,7 +334,7 @@ class TestSend: @pytest.mark.asyncio async def test_send_document_returns_explicit_unsupported_error(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) result = await adapter.send_document("chat-123", "/tmp/demo.pdf") @@ -352,7 +352,7 @@ class TestConnect: @pytest.mark.asyncio async def test_disconnect_closes_session_websocket(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) websocket = AsyncMock() @@ -376,16 +376,16 @@ class TestConnect: @pytest.mark.asyncio async def test_connect_fails_without_sdk(self, monkeypatch): monkeypatch.setattr( - "gateway.platforms.dingtalk.DINGTALK_STREAM_AVAILABLE", False + "plugins.platforms.dingtalk.adapter.DINGTALK_STREAM_AVAILABLE", False ) - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) result = await adapter.connect() assert result is False @pytest.mark.asyncio async def test_connect_fails_without_credentials(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) adapter._client_id = "" adapter._client_secret = "" @@ -394,7 +394,7 @@ class TestConnect: @pytest.mark.asyncio async def test_disconnect_cleans_up(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) adapter._session_webhooks["a"] = "http://x" adapter._dedup._seen["b"] = 1.0 @@ -410,7 +410,7 @@ class TestConnect: async def test_disconnect_finalizes_open_streaming_cards(self): """Streaming cards must be finalized before HTTP client closes.""" from unittest.mock import AsyncMock, patch - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) adapter._http_client = AsyncMock() adapter._stream_task = None @@ -456,29 +456,29 @@ class TestWebhookDomainAllowlist: """ def test_api_domain_accepted(self): - from gateway.platforms.dingtalk import _DINGTALK_WEBHOOK_RE + from plugins.platforms.dingtalk.adapter import _DINGTALK_WEBHOOK_RE assert _DINGTALK_WEBHOOK_RE.match( "https://api.dingtalk.com/robot/send?access_token=x" ) def test_oapi_domain_accepted(self): - from gateway.platforms.dingtalk import _DINGTALK_WEBHOOK_RE + from plugins.platforms.dingtalk.adapter import _DINGTALK_WEBHOOK_RE assert _DINGTALK_WEBHOOK_RE.match( "https://oapi.dingtalk.com/robot/send?access_token=x" ) def test_http_rejected(self): - from gateway.platforms.dingtalk import _DINGTALK_WEBHOOK_RE + from plugins.platforms.dingtalk.adapter import _DINGTALK_WEBHOOK_RE assert not _DINGTALK_WEBHOOK_RE.match("http://api.dingtalk.com/robot/send") def test_suffix_attack_rejected(self): - from gateway.platforms.dingtalk import _DINGTALK_WEBHOOK_RE + from plugins.platforms.dingtalk.adapter import _DINGTALK_WEBHOOK_RE assert not _DINGTALK_WEBHOOK_RE.match( "https://api.dingtalk.com.evil.example/" ) def test_unsanctioned_subdomain_rejected(self): - from gateway.platforms.dingtalk import _DINGTALK_WEBHOOK_RE + from plugins.platforms.dingtalk.adapter import _DINGTALK_WEBHOOK_RE # Only api.* and oapi.* are allowed — e.g. eapi.dingtalk.com must not slip through assert not _DINGTALK_WEBHOOK_RE.match("https://eapi.dingtalk.com/robot/send") @@ -487,7 +487,7 @@ class TestHandlerProcessIsAsync: """dingtalk-stream >= 0.20 requires ``process`` to be a coroutine.""" def test_process_is_coroutine_function(self): - from gateway.platforms.dingtalk import _IncomingHandler + from plugins.platforms.dingtalk.adapter import _IncomingHandler assert asyncio.iscoroutinefunction(_IncomingHandler.process) @@ -501,7 +501,7 @@ class TestExtractText: """ def test_text_as_dict_legacy(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter msg = MagicMock() msg.text = {"content": "hello world"} msg.rich_text_content = None @@ -510,7 +510,7 @@ class TestExtractText: def test_text_as_textcontent_object(self): """SDK >= 0.20 shape: object with ``.content`` attribute.""" - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter class FakeTextContent: content = "hello from new sdk" @@ -527,7 +527,7 @@ class TestExtractText: assert "TextContent(" not in result def test_text_content_attr_with_empty_string(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter class FakeTextContent: content = "" @@ -540,7 +540,7 @@ class TestExtractText: def test_rich_text_content_new_shape(self): """SDK >= 0.20 exposes rich text as ``message.rich_text_content.rich_text_list``.""" - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter class FakeRichText: rich_text_list = [{"text": "hello "}, {"text": "world"}] @@ -554,7 +554,7 @@ class TestExtractText: def test_rich_text_legacy_shape(self): """Legacy ``message.rich_text`` list remains supported.""" - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter msg = MagicMock() msg.text = None msg.rich_text_content = None @@ -563,7 +563,7 @@ class TestExtractText: assert "legacy" in result and "rich" in result def test_empty_message(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter msg = MagicMock() msg.text = None msg.rich_text_content = None @@ -586,7 +586,7 @@ class TestExtractMedia: def test_voice_rich_text_item_classified_as_voice(self): """Native DingTalk voice notes (type=voice) must enter the auto-STT path via MessageType.VOICE — the gateway skips STT for AUDIO.""" - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter from gateway.platforms.base import MessageType msg = self._msg_with_rich_text( @@ -602,7 +602,7 @@ class TestExtractMedia: def test_audio_rich_text_item_stays_audio(self): """Generic audio uploads (e.g. an mp3 the user attached) must NOT be auto-transcribed — they stay MessageType.AUDIO.""" - from gateway.platforms.dingtalk import DingTalkAdapter, DINGTALK_TYPE_MAPPING + from plugins.platforms.dingtalk.adapter import DingTalkAdapter, DINGTALK_TYPE_MAPPING from gateway.platforms.base import MessageType # Simulate a future/non-voice audio rich-text item by extending the @@ -643,7 +643,7 @@ def _make_gating_adapter(monkeypatch, *, extra=None, env=None): monkeypatch.delenv(key, raising=False) for key, value in (env or {}).items(): monkeypatch.setenv(key, value) - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter return DingTalkAdapter(PlatformConfig(enabled=True, extra=extra or {})) @@ -790,7 +790,7 @@ class TestIncomingHandlerProcess: @pytest.mark.asyncio async def test_process_extracts_session_webhook(self): """session_webhook must be populated from callback data.""" - from gateway.platforms.dingtalk import _IncomingHandler, DingTalkAdapter + from plugins.platforms.dingtalk.adapter import _IncomingHandler, DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) adapter._on_message = AsyncMock() @@ -823,7 +823,7 @@ class TestIncomingHandlerProcess: """If ChatbotMessage.from_dict does not map sessionWebhook (e.g. SDK version mismatch), the handler should fall back to extracting it directly from the raw data dict.""" - from gateway.platforms.dingtalk import _IncomingHandler, DingTalkAdapter + from plugins.platforms.dingtalk.adapter import _IncomingHandler, DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) adapter._on_message = AsyncMock() @@ -851,7 +851,7 @@ class TestIncomingHandlerProcess: async def test_process_returns_ack_immediately(self): """process() must not block on _on_message — it should return the ACK tuple before the message is fully processed.""" - from gateway.platforms.dingtalk import _IncomingHandler, DingTalkAdapter + from plugins.platforms.dingtalk.adapter import _IncomingHandler, DingTalkAdapter processing_started = asyncio.Event() processing_gate = asyncio.Event() @@ -895,7 +895,7 @@ class TestExtractTextMentions: Stripping all @handles collateral-damages emails, SSH URLs, and literal references the user wrote. """ - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter cases = [ ("@bot hello", "@bot hello"), ("contact alice@example.com", "contact alice@example.com"), @@ -928,7 +928,7 @@ class TestMessageContextIsolation: def test_contexts_keyed_by_chat_id(self): """Two concurrent chats must not clobber each other's context.""" - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(PlatformConfig(enabled=True)) msg_a = MagicMock(conversation_id="chat-A", sender_staff_id="user-A") @@ -953,7 +953,7 @@ class TestCardLifecycle: @pytest.fixture def adapter_with_card(self): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter a = DingTalkAdapter(PlatformConfig( enabled=True, extra={"card_template_id": "tmpl-1"}, @@ -1144,7 +1144,7 @@ class TestDingTalkAdapterAICards: @pytest.mark.asyncio async def test_send_uses_ai_card_if_configured(self, config, mock_stream_client, mock_http_client, mock_message): - from gateway.platforms.dingtalk import DingTalkAdapter + from plugins.platforms.dingtalk.adapter import DingTalkAdapter adapter = DingTalkAdapter(config) adapter._stream_client = mock_stream_client diff --git a/tests/gateway/test_dm_topics.py b/tests/gateway/test_dm_topics.py index 3f6b0942803c..d994cb257dea 100644 --- a/tests/gateway/test_dm_topics.py +++ b/tests/gateway/test_dm_topics.py @@ -40,12 +40,12 @@ def _ensure_telegram_mock(): sys.modules["telegram.request"] = telegram_mod.request # Force reimport so the adapter picks up the mock ChatType. - sys.modules.pop("gateway.platforms.telegram", None) + sys.modules.pop("plugins.platforms.telegram.adapter", None) _ensure_telegram_mock() -from gateway.platforms.telegram import TelegramAdapter # noqa: E402 +from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402 def _make_adapter(dm_topics_config=None, group_topics_config=None): diff --git a/tests/gateway/test_feishu.py b/tests/gateway/test_feishu.py index 4d78b454b0ca..bb97c7e72be1 100644 --- a/tests/gateway/test_feishu.py +++ b/tests/gateway/test_feishu.py @@ -81,7 +81,7 @@ class TestConfigEnvOverrides(unittest.TestCase): class TestFeishuMessageNormalization(unittest.TestCase): def test_normalize_merge_forward_preserves_summary_lines(self): - from gateway.platforms.feishu import normalize_feishu_message + from plugins.platforms.feishu.adapter import normalize_feishu_message normalized = normalize_feishu_message( message_type="merge_forward", @@ -111,7 +111,7 @@ class TestFeishuMessageNormalization(unittest.TestCase): ) def test_normalize_share_chat_exposes_summary_and_metadata(self): - from gateway.platforms.feishu import normalize_feishu_message + from plugins.platforms.feishu.adapter import normalize_feishu_message normalized = normalize_feishu_message( message_type="share_chat", @@ -129,7 +129,7 @@ class TestFeishuMessageNormalization(unittest.TestCase): self.assertEqual(normalized.metadata["chat_name"], "Backend Guild") def test_normalize_interactive_card_preserves_title_body_and_actions(self): - from gateway.platforms.feishu import normalize_feishu_message + from plugins.platforms.feishu.adapter import normalize_feishu_message normalized = normalize_feishu_message( message_type="interactive", @@ -172,7 +172,7 @@ class TestFeishuAdapterMessaging(unittest.TestCase): }, clear=True) def test_connect_webhook_mode_starts_local_server(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) runner = AsyncMock() @@ -184,14 +184,14 @@ class TestFeishuAdapterMessaging(unittest.TestCase): ) with ( - patch("gateway.platforms.feishu.FEISHU_AVAILABLE", True), - patch("gateway.platforms.feishu.FEISHU_WEBHOOK_AVAILABLE", True), - patch("gateway.platforms.feishu.EventDispatcherHandler") as mock_handler_class, - patch("gateway.platforms.feishu.acquire_scoped_lock", return_value=(True, None)), - patch("gateway.platforms.feishu.release_scoped_lock"), + patch("plugins.platforms.feishu.adapter.FEISHU_AVAILABLE", True), + patch("plugins.platforms.feishu.adapter.FEISHU_WEBHOOK_AVAILABLE", True), + patch("plugins.platforms.feishu.adapter.EventDispatcherHandler") as mock_handler_class, + patch("plugins.platforms.feishu.adapter.acquire_scoped_lock", return_value=(True, None)), + patch("plugins.platforms.feishu.adapter.release_scoped_lock"), patch.object(adapter, "_hydrate_bot_identity", new=AsyncMock()), patch.object(adapter, "_build_lark_client", return_value=SimpleNamespace()), - patch("gateway.platforms.feishu.web", web_module), + patch("plugins.platforms.feishu.adapter.web", web_module), ): _mock_event_dispatcher_builder(mock_handler_class) connected = asyncio.run(adapter.connect()) @@ -206,20 +206,20 @@ class TestFeishuAdapterMessaging(unittest.TestCase): }, clear=True) def test_connect_acquires_scoped_lock_and_disconnect_releases_it(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) ws_client = SimpleNamespace() with ( - patch("gateway.platforms.feishu.FEISHU_AVAILABLE", True), - patch("gateway.platforms.feishu.FEISHU_WEBSOCKET_AVAILABLE", True), - patch("gateway.platforms.feishu.lark", SimpleNamespace(LogLevel=SimpleNamespace(INFO="INFO", WARNING="WARNING"))), - patch("gateway.platforms.feishu.EventDispatcherHandler") as mock_handler_class, - patch("gateway.platforms.feishu.FeishuWSClient", return_value=ws_client), - patch("gateway.platforms.feishu._run_official_feishu_ws_client"), - patch("gateway.platforms.feishu.acquire_scoped_lock", return_value=(True, None)) as acquire_lock, - patch("gateway.platforms.feishu.release_scoped_lock") as release_lock, + patch("plugins.platforms.feishu.adapter.FEISHU_AVAILABLE", True), + patch("plugins.platforms.feishu.adapter.FEISHU_WEBSOCKET_AVAILABLE", True), + patch("plugins.platforms.feishu.adapter.lark", SimpleNamespace(LogLevel=SimpleNamespace(INFO="INFO", WARNING="WARNING"))), + patch("plugins.platforms.feishu.adapter.EventDispatcherHandler") as mock_handler_class, + patch("plugins.platforms.feishu.adapter.FeishuWSClient", return_value=ws_client), + patch("plugins.platforms.feishu.adapter._run_official_feishu_ws_client"), + patch("plugins.platforms.feishu.adapter.acquire_scoped_lock", return_value=(True, None)) as acquire_lock, + patch("plugins.platforms.feishu.adapter.release_scoped_lock") as release_lock, patch.object(adapter, "_hydrate_bot_identity", new=AsyncMock()), patch.object(adapter, "_build_lark_client", return_value=SimpleNamespace()), ): @@ -237,7 +237,7 @@ class TestFeishuAdapterMessaging(unittest.TestCase): return False try: - with patch("gateway.platforms.feishu.asyncio.get_running_loop", return_value=_Loop()): + with patch("plugins.platforms.feishu.adapter.asyncio.get_running_loop", return_value=_Loop()): connected = asyncio.run(adapter.connect()) asyncio.run(adapter.disconnect()) finally: @@ -258,15 +258,15 @@ class TestFeishuAdapterMessaging(unittest.TestCase): }, clear=True) def test_connect_rejects_existing_app_lock(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) with ( - patch("gateway.platforms.feishu.FEISHU_AVAILABLE", True), - patch("gateway.platforms.feishu.FEISHU_WEBSOCKET_AVAILABLE", True), + patch("plugins.platforms.feishu.adapter.FEISHU_AVAILABLE", True), + patch("plugins.platforms.feishu.adapter.FEISHU_WEBSOCKET_AVAILABLE", True), patch( - "gateway.platforms.feishu.acquire_scoped_lock", + "plugins.platforms.feishu.adapter.acquire_scoped_lock", return_value=(False, {"pid": 4321}), ), ): @@ -283,22 +283,22 @@ class TestFeishuAdapterMessaging(unittest.TestCase): }, clear=True) def test_connect_retries_transient_startup_failure(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) ws_client = SimpleNamespace() sleeps = [] with ( - patch("gateway.platforms.feishu.FEISHU_AVAILABLE", True), - patch("gateway.platforms.feishu.FEISHU_WEBSOCKET_AVAILABLE", True), - patch("gateway.platforms.feishu.lark", SimpleNamespace(LogLevel=SimpleNamespace(INFO="INFO", WARNING="WARNING"))), - patch("gateway.platforms.feishu.EventDispatcherHandler") as mock_handler_class, - patch("gateway.platforms.feishu.FeishuWSClient", return_value=ws_client), - patch("gateway.platforms.feishu.acquire_scoped_lock", return_value=(True, None)), - patch("gateway.platforms.feishu.release_scoped_lock"), + patch("plugins.platforms.feishu.adapter.FEISHU_AVAILABLE", True), + patch("plugins.platforms.feishu.adapter.FEISHU_WEBSOCKET_AVAILABLE", True), + patch("plugins.platforms.feishu.adapter.lark", SimpleNamespace(LogLevel=SimpleNamespace(INFO="INFO", WARNING="WARNING"))), + patch("plugins.platforms.feishu.adapter.EventDispatcherHandler") as mock_handler_class, + patch("plugins.platforms.feishu.adapter.FeishuWSClient", return_value=ws_client), + patch("plugins.platforms.feishu.adapter.acquire_scoped_lock", return_value=(True, None)), + patch("plugins.platforms.feishu.adapter.release_scoped_lock"), patch.object(adapter, "_hydrate_bot_identity", new=AsyncMock()), - patch("gateway.platforms.feishu.asyncio.sleep", side_effect=lambda delay: sleeps.append(delay)), + patch("plugins.platforms.feishu.adapter.asyncio.sleep", side_effect=lambda delay: sleeps.append(delay)), patch.object(adapter, "_build_lark_client", return_value=SimpleNamespace()), ): _mock_event_dispatcher_builder(mock_handler_class) @@ -322,7 +322,7 @@ class TestFeishuAdapterMessaging(unittest.TestCase): fake_loop = _Loop() try: - with patch("gateway.platforms.feishu.asyncio.get_running_loop", return_value=fake_loop): + with patch("plugins.platforms.feishu.adapter.asyncio.get_running_loop", return_value=fake_loop): connected = asyncio.run(adapter.connect()) finally: loop.close() @@ -334,7 +334,7 @@ class TestFeishuAdapterMessaging(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_edit_message_updates_existing_feishu_message(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {} @@ -355,7 +355,7 @@ class TestFeishuAdapterMessaging(unittest.TestCase): async def _direct(func, *args, **kwargs): return func(*args, **kwargs) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run( adapter.edit_message( chat_id="oc_chat", @@ -376,7 +376,7 @@ class TestFeishuAdapterMessaging(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_edit_message_falls_back_to_text_when_post_update_is_rejected(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {"calls": []} @@ -399,7 +399,7 @@ class TestFeishuAdapterMessaging(unittest.TestCase): async def _direct(func, *args, **kwargs): return func(*args, **kwargs) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run( adapter.edit_message( chat_id="oc_chat", @@ -419,7 +419,7 @@ class TestFeishuAdapterMessaging(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_get_chat_info_uses_real_feishu_chat_api(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) @@ -443,7 +443,7 @@ class TestFeishuAdapterMessaging(unittest.TestCase): async def _direct(func, *args, **kwargs): return func(*args, **kwargs) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): info = asyncio.run(adapter.get_chat_info("oc_chat")) self.assertEqual(chat_api.request.chat_id, "oc_chat") @@ -453,7 +453,7 @@ class TestFeishuAdapterMessaging(unittest.TestCase): class TestAdapterModule(unittest.TestCase): def test_load_settings_uses_sdk_defaults_for_invalid_ws_reconnect_values(self): - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter settings = FeishuAdapter._load_settings( { @@ -466,7 +466,7 @@ class TestAdapterModule(unittest.TestCase): self.assertEqual(settings.ws_reconnect_interval, 120) def test_load_settings_accepts_custom_ws_reconnect_values(self): - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter settings = FeishuAdapter._load_settings( { @@ -479,7 +479,7 @@ class TestAdapterModule(unittest.TestCase): self.assertEqual(settings.ws_reconnect_interval, 3) def test_load_settings_accepts_custom_ws_ping_values(self): - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter settings = FeishuAdapter._load_settings( { @@ -492,7 +492,7 @@ class TestAdapterModule(unittest.TestCase): self.assertEqual(settings.ws_ping_timeout, 8) def test_load_settings_ignores_invalid_ws_ping_values(self): - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter settings = FeishuAdapter._load_settings( { @@ -547,7 +547,7 @@ class TestAdapterModule(unittest.TestCase): sys.modules["lark_oapi.ws"] = fake_ws_module sys.modules["lark_oapi.ws.client"] = fake_client_module try: - from gateway.platforms.feishu import _run_official_feishu_ws_client + from plugins.platforms.feishu.adapter import _run_official_feishu_ws_client _run_official_feishu_ws_client(fake_client, fake_adapter) finally: @@ -574,7 +574,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_build_event_handler_registers_reaction_and_card_processors(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) calls = [] @@ -630,7 +630,7 @@ class TestAdapterBehavior(unittest.TestCase): calls.append("builder") return _Builder() - with patch("gateway.platforms.feishu.EventDispatcherHandler", _Dispatcher): + with patch("plugins.platforms.feishu.adapter.EventDispatcherHandler", _Dispatcher): handler = adapter._build_event_handler() self.assertEqual(handler, "handler") @@ -656,7 +656,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_bot_origin_reactions_are_dropped_to_avoid_feedback_loops(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._loop = object() @@ -669,7 +669,7 @@ class TestAdapterBehavior(unittest.TestCase): ) data = SimpleNamespace(event=event) with patch( - "gateway.platforms.feishu.asyncio.run_coroutine_threadsafe" + "plugins.platforms.feishu.adapter.asyncio.run_coroutine_threadsafe" ) as run_threadsafe: adapter._on_reaction_event("im.message.reaction.created_v1", data) run_threadsafe.assert_not_called() @@ -680,7 +680,7 @@ class TestAdapterBehavior(unittest.TestCase): # not additionally swallow user-origin reactions just because their # emoji happens to collide with a lifecycle emoji. from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._loop = SimpleNamespace(is_closed=lambda: False) @@ -697,7 +697,7 @@ class TestAdapterBehavior(unittest.TestCase): return SimpleNamespace(add_done_callback=lambda _: None) with patch( - "gateway.platforms.feishu.asyncio.run_coroutine_threadsafe", + "plugins.platforms.feishu.adapter.asyncio.run_coroutine_threadsafe", side_effect=_close_coro_and_return_future, ) as run_threadsafe: adapter._on_reaction_event("im.message.reaction.created_v1", data) @@ -706,7 +706,7 @@ class TestAdapterBehavior(unittest.TestCase): def _build_reaction_adapter(self, *, msg_sender_id: str): """Build a FeishuAdapter wired up to return a single GET-message result.""" from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._app_id = "cli_self_app" @@ -767,7 +767,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {"FEISHU_GROUP_POLICY": "open"}, clear=True) def test_group_message_requires_mentions_even_when_policy_open(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) message = SimpleNamespace(mentions=[]) @@ -780,7 +780,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {"FEISHU_GROUP_POLICY": "open"}, clear=True) def test_group_message_with_other_user_mention_is_rejected_when_bot_identity_unknown(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) sender_id = SimpleNamespace(open_id="ou_any", user_id=None) @@ -804,7 +804,7 @@ class TestAdapterBehavior(unittest.TestCase): ) def test_group_message_allowlist_and_mention_both_required(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) # Mention without IDs — name fallback legitimately engages. @@ -834,7 +834,7 @@ class TestAdapterBehavior(unittest.TestCase): def test_per_group_allowlist_policy_gates_by_sender(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter config = PlatformConfig( extra={ @@ -870,7 +870,7 @@ class TestAdapterBehavior(unittest.TestCase): def test_per_group_blacklist_policy_blocks_specific_users(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter config = PlatformConfig( extra={ @@ -906,7 +906,7 @@ class TestAdapterBehavior(unittest.TestCase): def test_per_group_admin_only_policy_requires_admin(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter config = PlatformConfig( extra={ @@ -942,7 +942,7 @@ class TestAdapterBehavior(unittest.TestCase): def test_per_group_disabled_policy_blocks_all(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter config = PlatformConfig( extra={ @@ -978,7 +978,7 @@ class TestAdapterBehavior(unittest.TestCase): def test_global_admins_bypass_all_group_rules(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter config = PlatformConfig( extra={ @@ -1008,7 +1008,7 @@ class TestAdapterBehavior(unittest.TestCase): def test_default_group_policy_fallback_for_chats_without_explicit_rule(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter config = PlatformConfig( extra={ @@ -1033,7 +1033,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {"FEISHU_GROUP_POLICY": "open"}, clear=True) def test_group_message_matches_bot_open_id_when_configured(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._bot_open_id = "ou_bot" @@ -1061,7 +1061,7 @@ class TestAdapterBehavior(unittest.TestCase): the mention and the bot carry open_ids, IDs are authoritative — a same-name human with a different open_id must NOT admit.""" from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter # Case 1: bot has only a name (open_id not hydrated / not configured). # Name fallback is the only available signal for any mention. @@ -1115,7 +1115,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_extract_post_message_as_text(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) message = SimpleNamespace( @@ -1134,7 +1134,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_extract_post_message_uses_first_available_language_block(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) message = SimpleNamespace( @@ -1153,7 +1153,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_extract_post_message_with_rich_elements_does_not_drop_content(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) message = SimpleNamespace( @@ -1179,7 +1179,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_extract_post_message_downloads_embedded_resources(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._download_feishu_image = AsyncMock(return_value=("/tmp/feishu-image.png", "image/png")) @@ -1215,7 +1215,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_extract_merge_forward_message_as_text_summary(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) message = SimpleNamespace( @@ -1245,7 +1245,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_extract_share_chat_message_as_text_summary(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) message = SimpleNamespace( @@ -1264,7 +1264,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_extract_interactive_message_as_text_summary(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) message = SimpleNamespace( @@ -1298,7 +1298,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_extract_image_message_downloads_and_caches(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._download_feishu_image = AsyncMock(return_value=("/tmp/feishu-image.png", "image/png")) @@ -1322,7 +1322,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_extract_audio_message_downloads_and_caches(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._download_feishu_message_resource = AsyncMock( @@ -1344,7 +1344,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_extract_file_message_downloads_and_caches(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._download_feishu_message_resource = AsyncMock( @@ -1366,7 +1366,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_extract_media_message_with_image_mime_becomes_photo(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._download_feishu_message_resource = AsyncMock( @@ -1388,7 +1388,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_extract_media_message_with_video_mime_becomes_video(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._download_feishu_message_resource = AsyncMock( @@ -1410,7 +1410,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_extract_text_from_raw_content_uses_relation_message_fallbacks(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) @@ -1429,7 +1429,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_extract_text_message_starting_with_slash_becomes_command(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._dispatch_inbound_event = AsyncMock() @@ -1467,7 +1467,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_extract_text_file_injects_content(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as tmp: @@ -1485,7 +1485,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_message_event_submits_to_adapter_loop(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) @@ -1512,7 +1512,7 @@ class TestAdapterBehavior(unittest.TestCase): coro.close() return future - with patch("gateway.platforms.feishu.asyncio.run_coroutine_threadsafe", side_effect=_submit) as submit: + with patch("plugins.platforms.feishu.adapter.asyncio.run_coroutine_threadsafe", side_effect=_submit) as submit: adapter._on_message_event(data) self.assertTrue(submit.called) @@ -1520,7 +1520,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_webhook_request_uses_same_message_dispatch_path(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._on_message_event = Mock() @@ -1550,7 +1550,7 @@ class TestAdapterBehavior(unittest.TestCase): sending an attacker-controlled challenge string. """ from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) body = json.dumps({ @@ -1573,7 +1573,7 @@ class TestAdapterBehavior(unittest.TestCase): def test_process_inbound_message_uses_event_sender_identity_only(self): from gateway.config import PlatformConfig from gateway.platforms.base import MessageType - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._dispatch_inbound_event = AsyncMock() @@ -1619,7 +1619,7 @@ class TestAdapterBehavior(unittest.TestCase): def test_text_batch_merges_rapid_messages_into_single_event(self): from gateway.config import PlatformConfig from gateway.platforms.base import MessageEvent, MessageType - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter from gateway.session import SessionSource adapter = FeishuAdapter(PlatformConfig()) @@ -1637,7 +1637,7 @@ class TestAdapterBehavior(unittest.TestCase): return None async def _run() -> None: - with patch("gateway.platforms.feishu.asyncio.sleep", side_effect=_sleep): + with patch("plugins.platforms.feishu.adapter.asyncio.sleep", side_effect=_sleep): await adapter._dispatch_inbound_event( MessageEvent(text="A", message_type=MessageType.TEXT, source=source, message_id="om_1") ) @@ -1665,7 +1665,7 @@ class TestAdapterBehavior(unittest.TestCase): def test_text_batch_flushes_when_message_count_limit_is_hit(self): from gateway.config import PlatformConfig from gateway.platforms.base import MessageEvent, MessageType - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter from gateway.session import SessionSource adapter = FeishuAdapter(PlatformConfig()) @@ -1683,7 +1683,7 @@ class TestAdapterBehavior(unittest.TestCase): return None async def _run() -> None: - with patch("gateway.platforms.feishu.asyncio.sleep", side_effect=_sleep): + with patch("plugins.platforms.feishu.adapter.asyncio.sleep", side_effect=_sleep): await adapter._dispatch_inbound_event( MessageEvent(text="A", message_type=MessageType.TEXT, source=source, message_id="om_1") ) @@ -1709,7 +1709,7 @@ class TestAdapterBehavior(unittest.TestCase): def test_media_batch_merges_rapid_photo_messages(self): from gateway.config import PlatformConfig from gateway.platforms.base import MessageEvent, MessageType - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter from gateway.session import SessionSource adapter = FeishuAdapter(PlatformConfig()) @@ -1727,7 +1727,7 @@ class TestAdapterBehavior(unittest.TestCase): return None async def _run() -> None: - with patch("gateway.platforms.feishu.asyncio.sleep", side_effect=_sleep): + with patch("plugins.platforms.feishu.adapter.asyncio.sleep", side_effect=_sleep): await adapter._dispatch_inbound_event( MessageEvent( text="第一张", @@ -1763,13 +1763,13 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_image_downloads_then_uses_native_image_send(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter.send_image_file = AsyncMock(return_value=SimpleNamespace(success=True, message_id="om_img")) async def _run(): - with patch("gateway.platforms.feishu.cache_image_from_url", new=AsyncMock(return_value="/tmp/cached.png")): + with patch("plugins.platforms.feishu.adapter.cache_image_from_url", new=AsyncMock(return_value="/tmp/cached.png")): return await adapter.send_image("oc_chat", "https://example.com/cat.png", caption="cat") result = asyncio.run(_run()) @@ -1781,7 +1781,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_animation_degrades_to_document_send(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter.send_document = AsyncMock(return_value=SimpleNamespace(success=True, message_id="om_gif")) @@ -1809,7 +1809,7 @@ class TestAdapterBehavior(unittest.TestCase): eagerly buffers it; a future refactor to .stream() would silently read-after-close.""" from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter events: list[str] = [] @@ -1847,7 +1847,7 @@ class TestAdapterBehavior(unittest.TestCase): with patch("tools.url_safety.is_safe_url", return_value=True): with patch("httpx.AsyncClient", _FakeAsyncClient): with patch( - "gateway.platforms.feishu.cache_document_from_bytes", + "plugins.platforms.feishu.adapter.cache_document_from_bytes", return_value="/tmp/cached-doc.bin", ): return await adapter._download_remote_document( @@ -1867,7 +1867,7 @@ class TestAdapterBehavior(unittest.TestCase): def test_dedup_state_persists_across_adapter_restart(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter with tempfile.TemporaryDirectory() as temp_home: with patch.dict(os.environ, {"HERMES_HOME": temp_home}, clear=False): @@ -1879,7 +1879,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_process_inbound_group_message_keeps_group_type_when_chat_lookup_falls_back(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._dispatch_inbound_event = AsyncMock() @@ -1916,7 +1916,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_process_inbound_message_fetches_reply_to_text(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._dispatch_inbound_event = AsyncMock() @@ -1955,7 +1955,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_replies_in_thread_when_thread_metadata_present(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {} @@ -1979,7 +1979,7 @@ class TestAdapterBehavior(unittest.TestCase): async def _direct(func, *args, **kwargs): return func(*args, **kwargs) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run( adapter.send( chat_id="oc_chat", @@ -1996,7 +1996,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_uses_metadata_reply_target_for_threaded_feishu_topic(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {} @@ -2016,7 +2016,7 @@ class TestAdapterBehavior(unittest.TestCase): async def _direct(func, *args, **kwargs): return func(*args, **kwargs) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run( adapter.send( chat_id="oc_chat", @@ -2035,7 +2035,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_retries_transient_failure(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {"attempts": 0} @@ -2067,8 +2067,8 @@ class TestAdapterBehavior(unittest.TestCase): sleeps.append(delay) with ( - patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct), - patch("gateway.platforms.feishu.asyncio.sleep", side_effect=_sleep), + patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct), + patch("plugins.platforms.feishu.adapter.asyncio.sleep", side_effect=_sleep), ): result = asyncio.run(adapter.send(chat_id="oc_chat", content="hello retry")) @@ -2080,7 +2080,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_does_not_retry_deterministic_api_failure(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {"attempts": 0} @@ -2110,8 +2110,8 @@ class TestAdapterBehavior(unittest.TestCase): sleeps.append(delay) with ( - patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct), - patch("gateway.platforms.feishu.asyncio.sleep", side_effect=_sleep), + patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct), + patch("plugins.platforms.feishu.adapter.asyncio.sleep", side_effect=_sleep), ): result = asyncio.run(adapter.send(chat_id="oc_chat", content="bad payload")) @@ -2123,7 +2123,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_document_reply_uses_thread_flag(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {} @@ -2160,7 +2160,7 @@ class TestAdapterBehavior(unittest.TestCase): file_path = tmp.name try: - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run( adapter.send_document( chat_id="oc_chat", @@ -2178,7 +2178,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_document_uploads_file_and_sends_file_message(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {} @@ -2216,7 +2216,7 @@ class TestAdapterBehavior(unittest.TestCase): file_path = tmp.name try: - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run(adapter.send_document(chat_id="oc_chat", file_path=file_path)) finally: os.unlink(file_path) @@ -2232,7 +2232,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_document_with_caption_uses_single_post_message(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {} @@ -2269,7 +2269,7 @@ class TestAdapterBehavior(unittest.TestCase): file_path = tmp.name try: - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run( adapter.send_document(chat_id="oc_chat", file_path=file_path, caption="报告请看") ) @@ -2285,7 +2285,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_image_file_uploads_image_and_sends_image_message(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {} @@ -2323,7 +2323,7 @@ class TestAdapterBehavior(unittest.TestCase): image_path = tmp.name try: - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run(adapter.send_image_file(chat_id="oc_chat", image_path=image_path)) finally: os.unlink(image_path) @@ -2339,7 +2339,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_image_file_with_caption_uses_single_post_message(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {} @@ -2376,7 +2376,7 @@ class TestAdapterBehavior(unittest.TestCase): image_path = tmp.name try: - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run( adapter.send_image_file(chat_id="oc_chat", image_path=image_path, caption="截图说明") ) @@ -2392,7 +2392,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_video_uploads_file_and_sends_media_message(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {} @@ -2430,7 +2430,7 @@ class TestAdapterBehavior(unittest.TestCase): video_path = tmp.name try: - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run(adapter.send_video(chat_id="oc_chat", video_path=video_path)) finally: os.unlink(video_path) @@ -2443,7 +2443,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_voice_uploads_opus_and_sends_audio_message(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {} @@ -2481,7 +2481,7 @@ class TestAdapterBehavior(unittest.TestCase): audio_path = tmp.name try: - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run(adapter.send_voice(chat_id="oc_chat", audio_path=audio_path)) finally: os.unlink(audio_path) @@ -2494,7 +2494,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_build_post_payload_extracts_title_and_links(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) payload = json.loads(adapter._build_post_payload("# 标题\n访问 [文档](https://example.com)")) @@ -2505,7 +2505,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_build_post_payload_wraps_markdown_in_md_tag(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) payload = json.loads( @@ -2523,7 +2523,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_build_post_payload_keeps_full_markdown_text(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) payload = json.loads( @@ -2541,7 +2541,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_uses_post_for_inline_markdown(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {} @@ -2565,7 +2565,7 @@ class TestAdapterBehavior(unittest.TestCase): async def _direct(func, *args, **kwargs): return func(*args, **kwargs) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run( adapter.send( chat_id="oc_chat", @@ -2582,7 +2582,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_splits_fenced_code_blocks_into_separate_post_rows(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {} @@ -2616,7 +2616,7 @@ class TestAdapterBehavior(unittest.TestCase): "后续说明仍应保留。" ) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run( adapter.send( chat_id="oc_chat", @@ -2645,7 +2645,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_build_post_payload_keeps_fence_like_code_lines_inside_code_block(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) payload = json.loads( @@ -2666,7 +2666,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_build_post_payload_preserves_trailing_spaces_in_code_block(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) payload = json.loads( @@ -2687,7 +2687,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_build_post_payload_splits_multiple_fenced_code_blocks(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) payload = json.loads( @@ -2710,7 +2710,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_falls_back_to_text_when_post_payload_is_rejected(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {"calls": []} @@ -2736,7 +2736,7 @@ class TestAdapterBehavior(unittest.TestCase): async def _direct(func, *args, **kwargs): return func(*args, **kwargs) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run( adapter.send( chat_id="oc_chat", @@ -2755,7 +2755,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_falls_back_to_text_when_post_response_is_unsuccessful(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {"calls": []} @@ -2781,7 +2781,7 @@ class TestAdapterBehavior(unittest.TestCase): async def _direct(func, *args, **kwargs): return func(*args, **kwargs) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run( adapter.send( chat_id="oc_chat", @@ -2800,7 +2800,7 @@ class TestAdapterBehavior(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_send_uses_post_for_advanced_markdown_lines(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) captured = {} @@ -2824,7 +2824,7 @@ class TestAdapterBehavior(unittest.TestCase): async def _direct(func, *args, **kwargs): return func(*args, **kwargs) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run( adapter.send( chat_id="oc_chat", @@ -2854,7 +2854,7 @@ class TestHydrateBotIdentity(unittest.TestCase): def _make_adapter(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter return FeishuAdapter(PlatformConfig()) @@ -2978,12 +2978,12 @@ class TestPendingInboundQueue(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_event_queued_when_loop_not_ready(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._loop = None # Simulate "before start()" or "during reconnect" - with patch("gateway.platforms.feishu.threading.Thread") as thread_cls: + with patch("plugins.platforms.feishu.adapter.threading.Thread") as thread_cls: adapter._on_message_event(SimpleNamespace(tag="evt-1")) adapter._on_message_event(SimpleNamespace(tag="evt-2")) adapter._on_message_event(SimpleNamespace(tag="evt-3")) @@ -2998,7 +2998,7 @@ class TestPendingInboundQueue(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_drainer_replays_queued_events_when_loop_becomes_ready(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._loop = None @@ -3010,7 +3010,7 @@ class TestPendingInboundQueue(unittest.TestCase): # Queue three events while loop is None (simulate the race). events = [SimpleNamespace(tag=f"evt-{i}") for i in range(3)] - with patch("gateway.platforms.feishu.threading.Thread"): + with patch("plugins.platforms.feishu.adapter.threading.Thread"): for ev in events: adapter._on_message_event(ev) @@ -3029,7 +3029,7 @@ class TestPendingInboundQueue(unittest.TestCase): return future with patch( - "gateway.platforms.feishu.asyncio.run_coroutine_threadsafe", + "plugins.platforms.feishu.adapter.asyncio.run_coroutine_threadsafe", side_effect=_submit, ) as submit: adapter._drain_pending_inbound_events() @@ -3044,13 +3044,13 @@ class TestPendingInboundQueue(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_drainer_drops_queue_when_adapter_shuts_down(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._loop = None adapter._running = False # Shutdown state - with patch("gateway.platforms.feishu.threading.Thread"): + with patch("plugins.platforms.feishu.adapter.threading.Thread"): adapter._on_message_event(SimpleNamespace(tag="evt-lost")) self.assertEqual(len(adapter._pending_inbound_events), 1) @@ -3064,13 +3064,13 @@ class TestPendingInboundQueue(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_queue_cap_evicts_oldest_beyond_max_depth(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._loop = None adapter._pending_inbound_max_depth = 3 # Shrink for test - with patch("gateway.platforms.feishu.threading.Thread"): + with patch("plugins.platforms.feishu.adapter.threading.Thread"): for i in range(5): adapter._on_message_event(SimpleNamespace(tag=f"evt-{i}")) @@ -3084,7 +3084,7 @@ class TestPendingInboundQueue(unittest.TestCase): """When the loop is ready, events should dispatch directly without ever touching the pending queue.""" from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) @@ -3101,10 +3101,10 @@ class TestPendingInboundQueue(unittest.TestCase): return future with patch( - "gateway.platforms.feishu.asyncio.run_coroutine_threadsafe", + "plugins.platforms.feishu.adapter.asyncio.run_coroutine_threadsafe", side_effect=_submit, ) as submit, patch( - "gateway.platforms.feishu.threading.Thread" + "plugins.platforms.feishu.adapter.threading.Thread" ) as thread_cls: adapter._on_message_event(SimpleNamespace(tag="evt")) @@ -3121,7 +3121,7 @@ class TestWebhookSecurity(unittest.TestCase): def _make_adapter(self, encrypt_key: str = "") -> "FeishuAdapter": from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter with patch.dict(os.environ, {"FEISHU_APP_ID": "cli", "FEISHU_APP_SECRET": "sec", "FEISHU_ENCRYPT_KEY": encrypt_key}, clear=True): return FeishuAdapter(PlatformConfig()) @@ -3158,14 +3158,14 @@ class TestWebhookSecurity(unittest.TestCase): self.assertTrue(adapter._check_webhook_rate_limit("10.0.0.1")) def test_rate_limit_blocks_after_exceeding_max(self): - from gateway.platforms.feishu import _FEISHU_WEBHOOK_RATE_LIMIT_MAX + from plugins.platforms.feishu.adapter import _FEISHU_WEBHOOK_RATE_LIMIT_MAX adapter = self._make_adapter() for _ in range(_FEISHU_WEBHOOK_RATE_LIMIT_MAX): adapter._check_webhook_rate_limit("10.0.0.2") self.assertFalse(adapter._check_webhook_rate_limit("10.0.0.2")) def test_rate_limit_resets_after_window_expires(self): - from gateway.platforms.feishu import _FEISHU_WEBHOOK_RATE_LIMIT_MAX, _FEISHU_WEBHOOK_RATE_WINDOW_SECONDS + from plugins.platforms.feishu.adapter import _FEISHU_WEBHOOK_RATE_LIMIT_MAX, _FEISHU_WEBHOOK_RATE_WINDOW_SECONDS adapter = self._make_adapter() ip = "10.0.0.3" for _ in range(_FEISHU_WEBHOOK_RATE_LIMIT_MAX): @@ -3179,7 +3179,7 @@ class TestWebhookSecurity(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_webhook_request_rejects_oversized_body(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter, _FEISHU_WEBHOOK_MAX_BODY_BYTES + from plugins.platforms.feishu.adapter import FeishuAdapter, _FEISHU_WEBHOOK_MAX_BODY_BYTES adapter = FeishuAdapter(PlatformConfig()) # Simulate a request whose Content-Length already signals oversize. @@ -3193,7 +3193,7 @@ class TestWebhookSecurity(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_webhook_request_rejects_invalid_json(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) request = SimpleNamespace( @@ -3207,7 +3207,7 @@ class TestWebhookSecurity(unittest.TestCase): @patch.dict(os.environ, {"FEISHU_ENCRYPT_KEY": "secret"}, clear=True) def test_webhook_request_rejects_bad_signature(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) body = json.dumps({"header": {"event_type": "im.message.receive_v1"}}).encode() @@ -3223,7 +3223,7 @@ class TestWebhookSecurity(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_webhook_connect_requires_inbound_auth_secret(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter( PlatformConfig( @@ -3236,7 +3236,7 @@ class TestWebhookSecurity(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_webhook_loads_auth_secrets_from_platform_extra(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter( PlatformConfig( @@ -3257,7 +3257,7 @@ class TestWebhookSecurity(unittest.TestCase): def test_webhook_url_verification_challenge_passes_without_signature(self): """Challenge requests must succeed even when no encrypt_key is set.""" from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) body = json.dumps({"type": "url_verification", "challenge": "test_challenge_token"}).encode() @@ -3277,7 +3277,7 @@ class TestDedupTTL(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_duplicate_within_ttl_is_rejected(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) with patch.object(adapter, "_persist_seen_message_ids"): @@ -3288,7 +3288,7 @@ class TestDedupTTL(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_expired_entry_is_not_considered_duplicate(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter, _FEISHU_DEDUP_TTL_SECONDS + from plugins.platforms.feishu.adapter import FeishuAdapter, _FEISHU_DEDUP_TTL_SECONDS adapter = FeishuAdapter(PlatformConfig()) # Plant an entry that expired well past the TTL. @@ -3306,7 +3306,7 @@ class TestDedupTTL(unittest.TestCase): """ import tempfile from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter with tempfile.TemporaryDirectory() as temp_home: with patch.dict(os.environ, {"HERMES_HOME": temp_home}, clear=True): @@ -3332,7 +3332,7 @@ class TestDedupTTL(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_persist_saves_timestamps_as_dict(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) ts = time.time() @@ -3348,7 +3348,7 @@ class TestDedupTTL(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_load_backward_compat_list_format(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) with tempfile.TemporaryDirectory() as tmpdir: @@ -3366,7 +3366,7 @@ class TestGroupMentionAtAll(unittest.TestCase): @patch.dict(os.environ, {"FEISHU_GROUP_POLICY": "open"}, clear=True) def test_at_all_in_content_accepts_without_explicit_bot_mention(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) message = SimpleNamespace( @@ -3380,7 +3380,7 @@ class TestGroupMentionAtAll(unittest.TestCase): def test_at_all_still_requires_policy_gate(self): """@_all bypasses mention gating but NOT the allowlist policy.""" from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) message = SimpleNamespace(content='{"text":"@_all attention"}', mentions=[]) @@ -3399,7 +3399,7 @@ class TestSenderNameResolution(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_returns_none_when_client_is_none(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._client = None @@ -3409,7 +3409,7 @@ class TestSenderNameResolution(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_returns_cached_name_within_ttl(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._client = SimpleNamespace() @@ -3421,7 +3421,7 @@ class TestSenderNameResolution(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_fetches_and_caches_name_from_api(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) user_obj = SimpleNamespace(name="Bob", display_name=None, nickname=None, en_name=None) @@ -3441,7 +3441,7 @@ class TestSenderNameResolution(unittest.TestCase): contact=SimpleNamespace(v3=SimpleNamespace(user=_ContactAPI())) ) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run(adapter._resolve_sender_name_from_api("ou_bob")) self.assertEqual(result, "Bob") @@ -3450,7 +3450,7 @@ class TestSenderNameResolution(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_expired_cache_triggers_new_api_call(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) # Expired cache entry. @@ -3469,7 +3469,7 @@ class TestSenderNameResolution(unittest.TestCase): contact=SimpleNamespace(v3=SimpleNamespace(user=_ContactAPI())) ) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run(adapter._resolve_sender_name_from_api("ou_expired")) self.assertEqual(result, "NewName") @@ -3477,7 +3477,7 @@ class TestSenderNameResolution(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_api_failure_returns_none_without_raising(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) @@ -3492,7 +3492,7 @@ class TestSenderNameResolution(unittest.TestCase): async def _direct(func, *args, **kwargs): return func(*args, **kwargs) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run(adapter._resolve_sender_name_from_api("ou_broken")) self.assertIsNone(result) @@ -3513,7 +3513,7 @@ class TestBotNameResolution(unittest.TestCase): def _build_adapter_with_bots(self, bots: Dict[str, str]): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) calls = [] @@ -3528,7 +3528,7 @@ class TestBotNameResolution(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_returns_cached_bot_name_without_api_call(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) adapter._sender_name_cache["ou_peer"] = ("Peer Bot", time.time() + 600) @@ -3545,7 +3545,7 @@ class TestBotNameResolution(unittest.TestCase): async def _direct(func, *args, **kwargs): return func(*args, **kwargs) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run(adapter._resolve_sender_name_from_api("ou_peer", is_bot=True)) self.assertEqual(result, "Peer Bot") @@ -3558,7 +3558,7 @@ class TestBotNameResolution(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_api_failure_returns_none_and_does_not_poison_cache(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) @@ -3570,7 +3570,7 @@ class TestBotNameResolution(unittest.TestCase): async def _direct(func, *args, **kwargs): return func(*args, **kwargs) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run(adapter._resolve_sender_name_from_api("ou_peer", is_bot=True)) self.assertIsNone(result) @@ -3585,7 +3585,7 @@ class TestBotNameResolution(unittest.TestCase): async def _direct(func, *args, **kwargs): return func(*args, **kwargs) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run(adapter._resolve_sender_name_from_api("ou_ghost", is_bot=True)) self.assertIsNone(result) @@ -3599,7 +3599,7 @@ class TestBotNameResolution(unittest.TestCase): async def _direct(func, *args, **kwargs): return func(*args, **kwargs) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): first = asyncio.run(adapter._resolve_sender_name_from_api("ou_nameless", is_bot=True)) second = asyncio.run(adapter._resolve_sender_name_from_api("ou_nameless", is_bot=True)) @@ -3611,7 +3611,7 @@ class TestBotNameResolution(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_non_zero_code_returns_none(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) error_payload = b'{"code":99991663,"msg":"permission denied"}' @@ -3622,7 +3622,7 @@ class TestBotNameResolution(unittest.TestCase): async def _direct(func, *args, **kwargs): return func(*args, **kwargs) - with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct): + with patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct): result = asyncio.run(adapter._resolve_sender_name_from_api("ou_peer", is_bot=True)) self.assertIsNone(result) @@ -3645,7 +3645,7 @@ class TestProcessingReactions(unittest.TestCase): next_reaction_id: str = "r1", ): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) tracker = SimpleNamespace( @@ -3694,7 +3694,7 @@ class TestProcessingReactions(unittest.TestCase): async def _direct(func, *args, **kwargs): return func(*args, **kwargs) - return patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct) + return patch("plugins.platforms.feishu.adapter.asyncio.to_thread", side_effect=_direct) # ------------------------------------------------------------------ start @patch.dict(os.environ, {}, clear=True) @@ -3828,7 +3828,7 @@ class TestProcessingReactions(unittest.TestCase): # ------------------------------------------------------------- LRU bounds @patch.dict(os.environ, {}, clear=True) def test_cache_evicts_oldest_entry_beyond_size_limit(self): - from gateway.platforms.feishu import _FEISHU_PROCESSING_REACTION_CACHE_SIZE + from plugins.platforms.feishu.adapter import _FEISHU_PROCESSING_REACTION_CACHE_SIZE adapter, _ = self._build_adapter() counter = {"n": 0} @@ -3859,7 +3859,7 @@ class TestProcessingReactions(unittest.TestCase): class TestFeishuMentionMap(unittest.TestCase): def test_build_mentions_map_handles_at_all(self): - from gateway.platforms.feishu import _build_mentions_map, _FeishuBotIdentity, FeishuMentionRef + from plugins.platforms.feishu.adapter import _build_mentions_map, _FeishuBotIdentity, FeishuMentionRef mention = SimpleNamespace(key="@_all", id=None, name="") result = _build_mentions_map( @@ -3869,7 +3869,7 @@ class TestFeishuMentionMap(unittest.TestCase): self.assertEqual(result["@_all"], FeishuMentionRef(is_all=True)) def test_build_mentions_map_marks_self_by_open_id(self): - from gateway.platforms.feishu import _build_mentions_map, _FeishuBotIdentity + from plugins.platforms.feishu.adapter import _build_mentions_map, _FeishuBotIdentity mention = SimpleNamespace( key="@_user_1", @@ -3882,7 +3882,7 @@ class TestFeishuMentionMap(unittest.TestCase): self.assertEqual(ref.name, "Hermes") def test_build_mentions_map_marks_self_by_name_fallback(self): - from gateway.platforms.feishu import _build_mentions_map, _FeishuBotIdentity + from plugins.platforms.feishu.adapter import _build_mentions_map, _FeishuBotIdentity mention = SimpleNamespace( key="@_user_1", @@ -3897,7 +3897,7 @@ class TestFeishuMentionMap(unittest.TestCase): NOT be flagged as self when their open_id differs. Before the fix, name-match fired even when open_id was present and different, causing their messages to be silently stripped/dropped.""" - from gateway.platforms.feishu import _build_mentions_map, _FeishuBotIdentity + from plugins.platforms.feishu.adapter import _build_mentions_map, _FeishuBotIdentity human_with_same_name = SimpleNamespace( key="@_user_1", @@ -3915,7 +3915,7 @@ class TestFeishuMentionMap(unittest.TestCase): not have populated _bot_open_id yet. During that window, a mention carrying a real open_id should still match via name — otherwise @bot messages silently fail admission.""" - from gateway.platforms.feishu import _build_mentions_map, _FeishuBotIdentity + from plugins.platforms.feishu.adapter import _build_mentions_map, _FeishuBotIdentity bot_mention = SimpleNamespace( key="@_user_1", @@ -3930,7 +3930,7 @@ class TestFeishuMentionMap(unittest.TestCase): self.assertTrue(result["@_user_1"].is_self) def test_build_mentions_map_non_self_user(self): - from gateway.platforms.feishu import _build_mentions_map, _FeishuBotIdentity + from plugins.platforms.feishu.adapter import _build_mentions_map, _FeishuBotIdentity mention = SimpleNamespace( key="@_user_1", @@ -3943,12 +3943,12 @@ class TestFeishuMentionMap(unittest.TestCase): self.assertEqual(ref.name, "Alice") def test_build_mentions_map_returns_empty_for_none_input(self): - from gateway.platforms.feishu import _build_mentions_map, _FeishuBotIdentity + from plugins.platforms.feishu.adapter import _build_mentions_map, _FeishuBotIdentity self.assertEqual(_build_mentions_map(None, _FeishuBotIdentity(open_id="ou_bot")), {}) def test_build_mentions_map_tolerates_missing_id_object(self): - from gateway.platforms.feishu import _build_mentions_map, _FeishuBotIdentity + from plugins.platforms.feishu.adapter import _build_mentions_map, _FeishuBotIdentity mention = SimpleNamespace(key="@_user_9", id=None, name="") ref = _build_mentions_map([mention], _FeishuBotIdentity(open_id="ou_bot"))["@_user_9"] @@ -3958,7 +3958,7 @@ class TestFeishuMentionMap(unittest.TestCase): class TestFeishuMentionHint(unittest.TestCase): def test_hint_single_user(self): - from gateway.platforms.feishu import FeishuMentionRef, _build_mention_hint + from plugins.platforms.feishu.adapter import FeishuMentionRef, _build_mention_hint refs = [FeishuMentionRef(name="Alice", open_id="ou_alice")] self.assertEqual( @@ -3967,7 +3967,7 @@ class TestFeishuMentionHint(unittest.TestCase): ) def test_hint_multiple_users(self): - from gateway.platforms.feishu import FeishuMentionRef, _build_mention_hint + from plugins.platforms.feishu.adapter import FeishuMentionRef, _build_mention_hint refs = [ FeishuMentionRef(name="Alice", open_id="ou_alice"), @@ -3979,13 +3979,13 @@ class TestFeishuMentionHint(unittest.TestCase): ) def test_hint_at_all(self): - from gateway.platforms.feishu import FeishuMentionRef, _build_mention_hint + from plugins.platforms.feishu.adapter import FeishuMentionRef, _build_mention_hint refs = [FeishuMentionRef(is_all=True)] self.assertEqual(_build_mention_hint(refs), "[Mentioned: @all]") def test_hint_filters_self_mentions(self): - from gateway.platforms.feishu import FeishuMentionRef, _build_mention_hint + from plugins.platforms.feishu.adapter import FeishuMentionRef, _build_mention_hint refs = [ FeishuMentionRef(name="Hermes", open_id="ou_bot", is_self=True), @@ -3997,30 +3997,30 @@ class TestFeishuMentionHint(unittest.TestCase): ) def test_hint_returns_empty_when_only_self(self): - from gateway.platforms.feishu import FeishuMentionRef, _build_mention_hint + from plugins.platforms.feishu.adapter import FeishuMentionRef, _build_mention_hint refs = [FeishuMentionRef(name="Hermes", open_id="ou_bot", is_self=True)] self.assertEqual(_build_mention_hint(refs), "") def test_hint_returns_empty_for_no_refs(self): - from gateway.platforms.feishu import _build_mention_hint + from plugins.platforms.feishu.adapter import _build_mention_hint self.assertEqual(_build_mention_hint([]), "") def test_hint_falls_back_when_open_id_missing(self): - from gateway.platforms.feishu import FeishuMentionRef, _build_mention_hint + from plugins.platforms.feishu.adapter import FeishuMentionRef, _build_mention_hint refs = [FeishuMentionRef(name="Alice", open_id="")] self.assertEqual(_build_mention_hint(refs), "[Mentioned: Alice]") def test_hint_uses_unknown_placeholder_when_name_missing(self): - from gateway.platforms.feishu import FeishuMentionRef, _build_mention_hint + from plugins.platforms.feishu.adapter import FeishuMentionRef, _build_mention_hint refs = [FeishuMentionRef(name="", open_id="ou_xxx")] self.assertEqual(_build_mention_hint(refs), "[Mentioned: unknown (open_id=ou_xxx)]") def test_hint_dedupes_repeated_user(self): - from gateway.platforms.feishu import FeishuMentionRef, _build_mention_hint + from plugins.platforms.feishu.adapter import FeishuMentionRef, _build_mention_hint refs = [ FeishuMentionRef(name="Alice", open_id="ou_alice"), @@ -4033,7 +4033,7 @@ class TestFeishuMentionHint(unittest.TestCase): ) def test_hint_dedupes_repeated_at_all(self): - from gateway.platforms.feishu import FeishuMentionRef, _build_mention_hint + from plugins.platforms.feishu.adapter import FeishuMentionRef, _build_mention_hint refs = [FeishuMentionRef(is_all=True), FeishuMentionRef(is_all=True)] self.assertEqual(_build_mention_hint(refs), "[Mentioned: @all]") @@ -4041,7 +4041,7 @@ class TestFeishuMentionHint(unittest.TestCase): class TestFeishuStripLeadingSelf(unittest.TestCase): def _make_refs(self, *, self_name="Hermes", other_name=None): - from gateway.platforms.feishu import FeishuMentionRef + from plugins.platforms.feishu.adapter import FeishuMentionRef refs = [FeishuMentionRef(name=self_name, open_id="ou_bot", is_self=True)] if other_name: @@ -4049,19 +4049,19 @@ class TestFeishuStripLeadingSelf(unittest.TestCase): return refs def test_strips_leading_self(self): - from gateway.platforms.feishu import _strip_edge_self_mentions + from plugins.platforms.feishu.adapter import _strip_edge_self_mentions result = _strip_edge_self_mentions("@Hermes /help", self._make_refs()) self.assertEqual(result, "/help") def test_strips_consecutive_leading_self(self): - from gateway.platforms.feishu import _strip_edge_self_mentions + from plugins.platforms.feishu.adapter import _strip_edge_self_mentions result = _strip_edge_self_mentions("@Hermes @Hermes hi", self._make_refs()) self.assertEqual(result, "hi") def test_stops_at_first_non_self_token(self): - from gateway.platforms.feishu import _strip_edge_self_mentions + from plugins.platforms.feishu.adapter import _strip_edge_self_mentions result = _strip_edge_self_mentions( "@Hermes @Alice make a group", self._make_refs(other_name="Alice") @@ -4069,26 +4069,26 @@ class TestFeishuStripLeadingSelf(unittest.TestCase): self.assertEqual(result, "@Alice make a group") def test_preserves_mid_text_self(self): - from gateway.platforms.feishu import _strip_edge_self_mentions + from plugins.platforms.feishu.adapter import _strip_edge_self_mentions result = _strip_edge_self_mentions("check @Hermes said yesterday", self._make_refs()) self.assertEqual(result, "check @Hermes said yesterday") def test_strips_trailing_self_at_end_of_text(self): - from gateway.platforms.feishu import _strip_edge_self_mentions + from plugins.platforms.feishu.adapter import _strip_edge_self_mentions result = _strip_edge_self_mentions("look up docs @Hermes", self._make_refs()) self.assertEqual(result, "look up docs") def test_strips_trailing_self_with_terminal_punct(self): - from gateway.platforms.feishu import _strip_edge_self_mentions + from plugins.platforms.feishu.adapter import _strip_edge_self_mentions # Terminal punct after the mention — strip the mention, keep the punct. result = _strip_edge_self_mentions("look up docs @Hermes.", self._make_refs()) self.assertEqual(result, "look up docs.") def test_preserves_trailing_self_before_non_terminal_char(self): - from gateway.platforms.feishu import _strip_edge_self_mentions + from plugins.platforms.feishu.adapter import _strip_edge_self_mentions # Non-terminal char (here a Chinese particle) follows — preserve. result = _strip_edge_self_mentions( @@ -4097,25 +4097,25 @@ class TestFeishuStripLeadingSelf(unittest.TestCase): self.assertEqual(result, "please don't @Hermes anymore") def test_returns_input_when_refs_empty(self): - from gateway.platforms.feishu import _strip_edge_self_mentions + from plugins.platforms.feishu.adapter import _strip_edge_self_mentions self.assertEqual(_strip_edge_self_mentions("@Hermes /help", []), "@Hermes /help") def test_returns_input_when_no_self_refs(self): - from gateway.platforms.feishu import _strip_edge_self_mentions, FeishuMentionRef + from plugins.platforms.feishu.adapter import _strip_edge_self_mentions, FeishuMentionRef refs = [FeishuMentionRef(name="Alice", open_id="ou_alice")] self.assertEqual(_strip_edge_self_mentions("@Alice hi", refs), "@Alice hi") def test_uses_open_id_fallback_when_name_missing(self): - from gateway.platforms.feishu import _strip_edge_self_mentions, FeishuMentionRef + from plugins.platforms.feishu.adapter import _strip_edge_self_mentions, FeishuMentionRef refs = [FeishuMentionRef(name="", open_id="ou_bot", is_self=True)] self.assertEqual(_strip_edge_self_mentions("@ou_bot hi", refs), "hi") def test_word_boundary_prevents_prefix_collision(self): """A bot named 'Al' must not eat the leading '@Alice' of a different user.""" - from gateway.platforms.feishu import _strip_edge_self_mentions, FeishuMentionRef + from plugins.platforms.feishu.adapter import _strip_edge_self_mentions, FeishuMentionRef refs = [FeishuMentionRef(name="Al", open_id="ou_bot", is_self=True)] self.assertEqual(_strip_edge_self_mentions("@Alice hi", refs), "@Alice hi") @@ -4123,13 +4123,13 @@ class TestFeishuStripLeadingSelf(unittest.TestCase): class TestFeishuNormalizeText(unittest.TestCase): def test_renders_mention_with_display_name(self): - from gateway.platforms.feishu import _normalize_feishu_text, FeishuMentionRef + from plugins.platforms.feishu.adapter import _normalize_feishu_text, FeishuMentionRef refs = {"@_user_1": FeishuMentionRef(name="Alice", open_id="ou_alice")} self.assertEqual(_normalize_feishu_text("@_user_1 hello", refs), "@Alice hello") def test_renders_self_mention_with_name(self): - from gateway.platforms.feishu import _normalize_feishu_text, FeishuMentionRef + from plugins.platforms.feishu.adapter import _normalize_feishu_text, FeishuMentionRef refs = {"@_user_1": FeishuMentionRef(name="Hermes", open_id="ou_bot", is_self=True)} self.assertEqual( @@ -4138,23 +4138,23 @@ class TestFeishuNormalizeText(unittest.TestCase): ) def test_at_all_rendered_as_english_literal(self): - from gateway.platforms.feishu import _normalize_feishu_text + from plugins.platforms.feishu.adapter import _normalize_feishu_text self.assertEqual(_normalize_feishu_text("@_all notice", None), "@all notice") def test_unknown_placeholder_degrades_to_space(self): - from gateway.platforms.feishu import _normalize_feishu_text + from plugins.platforms.feishu.adapter import _normalize_feishu_text # No map: fall back to the old behavior (substitute with space, then collapse). self.assertEqual(_normalize_feishu_text("@_user_9 hello", None), "hello") def test_backward_compatible_without_map(self): - from gateway.platforms.feishu import _normalize_feishu_text + from plugins.platforms.feishu.adapter import _normalize_feishu_text self.assertEqual(_normalize_feishu_text("hello world"), "hello world") def test_mention_for_missing_map_entry_degrades_to_space(self): - from gateway.platforms.feishu import _normalize_feishu_text, FeishuMentionRef + from plugins.platforms.feishu.adapter import _normalize_feishu_text, FeishuMentionRef refs = {"@_user_1": FeishuMentionRef(name="Alice")} # @_user_2 has no entry — should degrade to a space (legacy behavior) @@ -4169,7 +4169,7 @@ class TestFeishuPostMentionParsing(unittest.TestCase): """Post .user_id is a placeholder ('@_user_N'); the real display name comes from the mentions_map lookup. Confirmed via live im.v1.message.get payload.""" - from gateway.platforms.feishu import parse_feishu_post_payload, FeishuMentionRef + from plugins.platforms.feishu.adapter import parse_feishu_post_payload, FeishuMentionRef payload = { "en_us": { @@ -4188,7 +4188,7 @@ class TestFeishuPostMentionParsing(unittest.TestCase): def test_post_at_tag_falls_back_to_inline_user_name_when_map_misses(self): """When the mentions payload is missing a placeholder, fall back to the inline user_name in the tag itself.""" - from gateway.platforms.feishu import parse_feishu_post_payload + from plugins.platforms.feishu.adapter import parse_feishu_post_payload payload = { "en_us": { @@ -4204,7 +4204,7 @@ class TestFeishuPostMentionParsing(unittest.TestCase): def test_post_at_all_tag_renders_as_at_all(self): """Post-format @everyone has user_id == '@_all' (confirmed via live im.v1.message.get). Rendered as literal '@all' regardless of map.""" - from gateway.platforms.feishu import parse_feishu_post_payload + from plugins.platforms.feishu.adapter import parse_feishu_post_payload payload = { "en_us": { @@ -4220,7 +4220,7 @@ class TestFeishuPostMentionParsing(unittest.TestCase): class TestFeishuNormalizeWithMentions(unittest.TestCase): def test_text_message_renders_mention_by_name(self): - from gateway.platforms.feishu import normalize_feishu_message, _FeishuBotIdentity + from plugins.platforms.feishu.adapter import normalize_feishu_message, _FeishuBotIdentity mention = SimpleNamespace( key="@_user_1", @@ -4239,7 +4239,7 @@ class TestFeishuNormalizeWithMentions(unittest.TestCase): self.assertFalse(normalized.mentions[0].is_self) def test_text_message_marks_bot_self_mention(self): - from gateway.platforms.feishu import normalize_feishu_message, _FeishuBotIdentity + from plugins.platforms.feishu.adapter import normalize_feishu_message, _FeishuBotIdentity mention = SimpleNamespace( key="@_user_1", @@ -4257,7 +4257,7 @@ class TestFeishuNormalizeWithMentions(unittest.TestCase): self.assertEqual(normalized.text_content, "@Hermes /help") def test_text_message_at_all_surfaces_ref(self): - from gateway.platforms.feishu import normalize_feishu_message + from plugins.platforms.feishu.adapter import normalize_feishu_message mention = SimpleNamespace(key="@_all", id=None, name="") normalized = normalize_feishu_message( @@ -4273,7 +4273,7 @@ class TestFeishuNormalizeWithMentions(unittest.TestCase): """Feishu SDK sometimes omits @_all from the mentions payload (confirmed via im.v1.message.get). The fallback scan on raw text must still yield an is_all ref so [Mentioned: @all] gets injected.""" - from gateway.platforms.feishu import normalize_feishu_message + from plugins.platforms.feishu.adapter import normalize_feishu_message normalized = normalize_feishu_message( message_type="text", @@ -4286,7 +4286,7 @@ class TestFeishuNormalizeWithMentions(unittest.TestCase): def test_text_message_at_all_not_synthesized_if_absent_from_text(self): """No @_all in text → no synthetic ref even if mentions_map is empty.""" - from gateway.platforms.feishu import normalize_feishu_message + from plugins.platforms.feishu.adapter import normalize_feishu_message normalized = normalize_feishu_message( message_type="text", @@ -4296,7 +4296,7 @@ class TestFeishuNormalizeWithMentions(unittest.TestCase): self.assertEqual(normalized.mentions, []) def test_text_message_without_mentions_param_is_backward_compatible(self): - from gateway.platforms.feishu import normalize_feishu_message + from plugins.platforms.feishu.adapter import normalize_feishu_message normalized = normalize_feishu_message( message_type="text", @@ -4308,7 +4308,7 @@ class TestFeishuNormalizeWithMentions(unittest.TestCase): def test_post_message_marks_self_via_mentions_map_lookup(self): """Real Feishu post: + top-level mentions array resolves to open_id via placeholder lookup, not direct tag fields.""" - from gateway.platforms.feishu import normalize_feishu_message, _FeishuBotIdentity + from plugins.platforms.feishu.adapter import normalize_feishu_message, _FeishuBotIdentity raw = json.dumps({ "en_us": { @@ -4338,7 +4338,7 @@ class TestFeishuNormalizeWithMentions(unittest.TestCase): class TestFeishuPostMentionsBot(unittest.TestCase): def _build_adapter(self, bot_open_id="ou_bot", bot_user_id="", bot_name=""): - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter.__new__(FeishuAdapter) adapter._bot_open_id = bot_open_id @@ -4347,7 +4347,7 @@ class TestFeishuPostMentionsBot(unittest.TestCase): return adapter def test_post_mentions_bot_uses_is_self_flag(self): - from gateway.platforms.feishu import FeishuMentionRef + from plugins.platforms.feishu.adapter import FeishuMentionRef adapter = self._build_adapter() self.assertTrue( @@ -4368,7 +4368,7 @@ class TestFeishuPostMentionsBot(unittest.TestCase): class TestFeishuExtractMessageContent(unittest.TestCase): def _build_adapter(self): - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter.__new__(FeishuAdapter) adapter._bot_open_id = "ou_bot" @@ -4415,7 +4415,7 @@ class TestFeishuExtractMessageContent(unittest.TestCase): class TestFeishuProcessInboundMessage(unittest.TestCase): def _build_adapter(self): - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter.__new__(FeishuAdapter) adapter._bot_open_id = "ou_bot" @@ -4599,7 +4599,7 @@ class TestFeishuProcessInboundMessage(unittest.TestCase): class TestFeishuFetchMessageText(unittest.TestCase): def _build_adapter(self): - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter.__new__(FeishuAdapter) adapter._bot_open_id = "ou_bot" @@ -4635,7 +4635,7 @@ class TestFeishuFetchMessageText(unittest.TestCase): self.assertNotIn("[Mentioned:", result) def test_extract_text_from_raw_content_accepts_mentions_kwarg(self): - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter.__new__(FeishuAdapter) adapter._bot_open_id = "" @@ -4686,7 +4686,7 @@ class TestFeishuFetchMessageText(unittest.TestCase): """_build_mentions_map accepts the reply-history shape (id as str + id_type='open_id'). user_id id_type is not load-bearing for self detection — inbound mention payloads always include an open_id.""" - from gateway.platforms.feishu import _build_mentions_map, _FeishuBotIdentity + from plugins.platforms.feishu.adapter import _build_mentions_map, _FeishuBotIdentity # open_id discriminator, non-self alice = SimpleNamespace(key="@_user_1", id="ou_alice", id_type="open_id", name="Alice") @@ -4705,7 +4705,7 @@ class TestFeishuMentionEndToEnd(unittest.TestCase): """High-level scenarios from the design spec — verify the full pipeline.""" def _build_adapter(self): - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter.__new__(FeishuAdapter) adapter._bot_open_id = "ou_bot" @@ -4893,7 +4893,7 @@ class TestChatLockEviction(unittest.TestCase): def _make_adapter(self, max_size=5): import collections as _collections - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = object.__new__(FeishuAdapter) adapter._chat_locks = _collections.OrderedDict() diff --git a/tests/gateway/test_feishu_approval_buttons.py b/tests/gateway/test_feishu_approval_buttons.py index 999ac648d238..f5b9a26c1e12 100644 --- a/tests/gateway/test_feishu_approval_buttons.py +++ b/tests/gateway/test_feishu_approval_buttons.py @@ -38,8 +38,8 @@ def _ensure_feishu_mocks(): _ensure_feishu_mocks() from gateway.config import PlatformConfig -import gateway.platforms.feishu as feishu_module -from gateway.platforms.feishu import FeishuAdapter +import plugins.platforms.feishu.adapter as feishu_module +from plugins.platforms.feishu.adapter import FeishuAdapter # --------------------------------------------------------------------------- diff --git a/tests/gateway/test_feishu_bot_admission.py b/tests/gateway/test_feishu_bot_admission.py index 2d71ad06de18..61628f933a82 100644 --- a/tests/gateway/test_feishu_bot_admission.py +++ b/tests/gateway/test_feishu_bot_admission.py @@ -28,7 +28,7 @@ from tests.gateway.feishu_helpers import ( ], ) def test_feishu_load_settings_populates_allow_bots(monkeypatch, env_value, expected): - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter monkeypatch.setenv("FEISHU_APP_ID", "cli_test") monkeypatch.setenv("FEISHU_APP_SECRET", "secret_test") @@ -39,7 +39,7 @@ def test_feishu_load_settings_populates_allow_bots(monkeypatch, env_value, expec def test_feishu_load_settings_allow_bots_defaults_to_none(monkeypatch): - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter monkeypatch.setenv("FEISHU_APP_ID", "cli_test") monkeypatch.setenv("FEISHU_APP_SECRET", "secret_test") @@ -51,7 +51,7 @@ def test_feishu_load_settings_allow_bots_defaults_to_none(monkeypatch): def test_feishu_load_settings_ignores_extra_allow_bots(monkeypatch): # extra is ignored — env is single source of truth (yaml is bridged to env). - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter monkeypatch.setenv("FEISHU_APP_ID", "cli_test") monkeypatch.setenv("FEISHU_APP_SECRET", "secret_test") @@ -62,7 +62,7 @@ def test_feishu_load_settings_ignores_extra_allow_bots(monkeypatch): def test_feishu_load_settings_falls_back_to_env_when_extra_missing(monkeypatch): - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter monkeypatch.setenv("FEISHU_APP_ID", "cli_test") monkeypatch.setenv("FEISHU_APP_SECRET", "secret_test") @@ -75,13 +75,13 @@ def test_feishu_load_settings_falls_back_to_env_when_extra_missing(monkeypatch): def test_feishu_load_settings_warns_on_unknown_allow_bots(monkeypatch, caplog): import logging - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter monkeypatch.setenv("FEISHU_APP_ID", "cli_test") monkeypatch.setenv("FEISHU_APP_SECRET", "secret_test") monkeypatch.setenv("FEISHU_ALLOW_BOTS", "menton") # typo - with caplog.at_level(logging.WARNING, logger="gateway.platforms.feishu"): + with caplog.at_level(logging.WARNING, logger="plugins.platforms.feishu.adapter"): settings = FeishuAdapter._load_settings(extra={}) assert settings.allow_bots == "none" @@ -98,7 +98,7 @@ def test_feishu_load_settings_warns_on_unknown_allow_bots(monkeypatch, caplog): ], ) def test_feishu_load_settings_require_mention(monkeypatch, env_value, extra, expected): - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter monkeypatch.setenv("FEISHU_APP_ID", "cli_test") monkeypatch.setenv("FEISHU_APP_SECRET", "secret_test") @@ -112,7 +112,7 @@ def test_feishu_load_settings_require_mention(monkeypatch, env_value, extra, exp def test_feishu_load_settings_parses_per_group_require_mention(monkeypatch): - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter monkeypatch.setenv("FEISHU_APP_ID", "cli_test") monkeypatch.setenv("FEISHU_APP_SECRET", "secret_test") @@ -133,7 +133,7 @@ def test_feishu_load_settings_parses_per_group_require_mention(monkeypatch): def test_sender_identity_collects_every_non_empty_id_variant(): - from gateway.platforms.feishu import _sender_identity + from plugins.platforms.feishu.adapter import _sender_identity sender = SimpleNamespace( sender_id=SimpleNamespace(open_id="ou_x", user_id="", union_id="un_x"), @@ -142,21 +142,21 @@ def test_sender_identity_collects_every_non_empty_id_variant(): def test_sender_identity_handles_missing_sender_id(): - from gateway.platforms.feishu import _sender_identity + from plugins.platforms.feishu.adapter import _sender_identity assert _sender_identity(SimpleNamespace()) == frozenset() @pytest.mark.parametrize("sender_type", ["bot", "app"]) def test_is_bot_sender_treats_bot_and_app_as_bot_origin(sender_type): - from gateway.platforms.feishu import _is_bot_sender + from plugins.platforms.feishu.adapter import _is_bot_sender assert _is_bot_sender(SimpleNamespace(sender_type=sender_type)) is True @pytest.mark.parametrize("sender_type", ["user", "", None]) def test_is_bot_sender_rejects_non_bot_origin(sender_type): - from gateway.platforms.feishu import _is_bot_sender + from plugins.platforms.feishu.adapter import _is_bot_sender assert _is_bot_sender(SimpleNamespace(sender_type=sender_type)) is False @@ -430,7 +430,7 @@ def test_admit_group_mention_checked_once_per_call(): def test_admit_per_group_require_mention_overrides_global(): - from gateway.platforms.feishu import FeishuGroupRule + from plugins.platforms.feishu.adapter import FeishuGroupRule adapter = make_adapter_skeleton( bot_open_id="ou_self", require_mention=True, group_policy="open", @@ -454,7 +454,7 @@ def test_admit_per_group_require_mention_overrides_global(): def test_hydrate_bot_identity_populates_self_ids_from_bot_v3_info(monkeypatch): import asyncio - from gateway.platforms import feishu as feishu_mod + import plugins.platforms.feishu.adapter as feishu_mod FeishuAdapter = feishu_mod.FeishuAdapter class _FakeBaseRequestBuilder: @@ -515,7 +515,7 @@ def test_hydrate_bot_identity_populates_self_ids_from_bot_v3_info(monkeypatch): def test_resolve_sender_profile_uses_open_id_for_bot_name_lookup(): import asyncio - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = object.__new__(FeishuAdapter) adapter._client = object() @@ -569,7 +569,7 @@ def _group_case( def _group_rule(policy: str, **kwargs): - from gateway.platforms.feishu import FeishuGroupRule + from plugins.platforms.feishu.adapter import FeishuGroupRule return FeishuGroupRule(policy=policy, **kwargs) diff --git a/tests/gateway/test_feishu_comment.py b/tests/gateway/test_feishu_comment.py index 6241de6f86e2..320d1d56ab39 100644 --- a/tests/gateway/test_feishu_comment.py +++ b/tests/gateway/test_feishu_comment.py @@ -5,7 +5,7 @@ import unittest from types import SimpleNamespace from unittest.mock import AsyncMock, Mock, patch -from gateway.platforms.feishu_comment import ( +from plugins.platforms.feishu.feishu_comment import ( parse_drive_comment_event, _ALLOWED_NOTICE_TYPES, _sanitize_comment_text, @@ -62,45 +62,45 @@ class TestEventFiltering(unittest.TestCase): def _run(self, coro): return asyncio.get_event_loop().run_until_complete(coro) - @patch("gateway.platforms.feishu_comment_rules.load_config") - @patch("gateway.platforms.feishu_comment_rules.resolve_rule") - @patch("gateway.platforms.feishu_comment_rules.is_user_allowed") + @patch("plugins.platforms.feishu.feishu_comment_rules.load_config") + @patch("plugins.platforms.feishu.feishu_comment_rules.resolve_rule") + @patch("plugins.platforms.feishu.feishu_comment_rules.is_user_allowed") def test_self_reply_filtered(self, mock_allowed, mock_resolve, mock_load): """Events where from_open_id == self_open_id should be dropped.""" - from gateway.platforms.feishu_comment import handle_drive_comment_event + from plugins.platforms.feishu.feishu_comment import handle_drive_comment_event evt = _make_event(from_open_id="ou_bot", to_open_id="ou_bot") self._run(handle_drive_comment_event(Mock(), evt, self_open_id="ou_bot")) mock_load.assert_not_called() - @patch("gateway.platforms.feishu_comment_rules.load_config") - @patch("gateway.platforms.feishu_comment_rules.resolve_rule") - @patch("gateway.platforms.feishu_comment_rules.is_user_allowed") + @patch("plugins.platforms.feishu.feishu_comment_rules.load_config") + @patch("plugins.platforms.feishu.feishu_comment_rules.resolve_rule") + @patch("plugins.platforms.feishu.feishu_comment_rules.is_user_allowed") def test_wrong_receiver_filtered(self, mock_allowed, mock_resolve, mock_load): """Events where to_open_id != self_open_id should be dropped.""" - from gateway.platforms.feishu_comment import handle_drive_comment_event + from plugins.platforms.feishu.feishu_comment import handle_drive_comment_event evt = _make_event(to_open_id="ou_other_bot") self._run(handle_drive_comment_event(Mock(), evt, self_open_id="ou_bot")) mock_load.assert_not_called() - @patch("gateway.platforms.feishu_comment_rules.load_config") - @patch("gateway.platforms.feishu_comment_rules.resolve_rule") - @patch("gateway.platforms.feishu_comment_rules.is_user_allowed") + @patch("plugins.platforms.feishu.feishu_comment_rules.load_config") + @patch("plugins.platforms.feishu.feishu_comment_rules.resolve_rule") + @patch("plugins.platforms.feishu.feishu_comment_rules.is_user_allowed") def test_empty_to_open_id_filtered(self, mock_allowed, mock_resolve, mock_load): """Events with empty to_open_id should be dropped.""" - from gateway.platforms.feishu_comment import handle_drive_comment_event + from plugins.platforms.feishu.feishu_comment import handle_drive_comment_event evt = _make_event(to_open_id="") self._run(handle_drive_comment_event(Mock(), evt, self_open_id="ou_bot")) mock_load.assert_not_called() - @patch("gateway.platforms.feishu_comment_rules.load_config") - @patch("gateway.platforms.feishu_comment_rules.resolve_rule") - @patch("gateway.platforms.feishu_comment_rules.is_user_allowed") + @patch("plugins.platforms.feishu.feishu_comment_rules.load_config") + @patch("plugins.platforms.feishu.feishu_comment_rules.resolve_rule") + @patch("plugins.platforms.feishu.feishu_comment_rules.is_user_allowed") def test_invalid_notice_type_filtered(self, mock_allowed, mock_resolve, mock_load): """Events with unsupported notice_type should be dropped.""" - from gateway.platforms.feishu_comment import handle_drive_comment_event + from plugins.platforms.feishu.feishu_comment import handle_drive_comment_event evt = _make_event(notice_type="resolve_comment") self._run(handle_drive_comment_event(Mock(), evt, self_open_id="ou_bot")) @@ -116,14 +116,14 @@ class TestAccessControlIntegration(unittest.TestCase): def _run(self, coro): return asyncio.get_event_loop().run_until_complete(coro) - @patch("gateway.platforms.feishu_comment_rules.has_wiki_keys", return_value=False) - @patch("gateway.platforms.feishu_comment_rules.is_user_allowed", return_value=False) - @patch("gateway.platforms.feishu_comment_rules.resolve_rule") - @patch("gateway.platforms.feishu_comment_rules.load_config") + @patch("plugins.platforms.feishu.feishu_comment_rules.has_wiki_keys", return_value=False) + @patch("plugins.platforms.feishu.feishu_comment_rules.is_user_allowed", return_value=False) + @patch("plugins.platforms.feishu.feishu_comment_rules.resolve_rule") + @patch("plugins.platforms.feishu.feishu_comment_rules.load_config") def test_denied_user_no_side_effects(self, mock_load, mock_resolve, mock_allowed, mock_wiki_keys): """Denied user should not trigger typing reaction or agent.""" - from gateway.platforms.feishu_comment import handle_drive_comment_event - from gateway.platforms.feishu_comment_rules import ResolvedCommentRule + from plugins.platforms.feishu.feishu_comment import handle_drive_comment_event + from plugins.platforms.feishu.feishu_comment_rules import ResolvedCommentRule mock_resolve.return_value = ResolvedCommentRule(True, "allowlist", frozenset(), "top") mock_load.return_value = Mock() @@ -135,14 +135,14 @@ class TestAccessControlIntegration(unittest.TestCase): # No API calls should be made for denied users client.request.assert_not_called() - @patch("gateway.platforms.feishu_comment_rules.has_wiki_keys", return_value=False) - @patch("gateway.platforms.feishu_comment_rules.is_user_allowed", return_value=False) - @patch("gateway.platforms.feishu_comment_rules.resolve_rule") - @patch("gateway.platforms.feishu_comment_rules.load_config") + @patch("plugins.platforms.feishu.feishu_comment_rules.has_wiki_keys", return_value=False) + @patch("plugins.platforms.feishu.feishu_comment_rules.is_user_allowed", return_value=False) + @patch("plugins.platforms.feishu.feishu_comment_rules.resolve_rule") + @patch("plugins.platforms.feishu.feishu_comment_rules.load_config") def test_disabled_comment_skipped(self, mock_load, mock_resolve, mock_allowed, mock_wiki_keys): """Disabled comments should return immediately.""" - from gateway.platforms.feishu_comment import handle_drive_comment_event - from gateway.platforms.feishu_comment_rules import ResolvedCommentRule + from plugins.platforms.feishu.feishu_comment import handle_drive_comment_event + from plugins.platforms.feishu.feishu_comment_rules import ResolvedCommentRule mock_resolve.return_value = ResolvedCommentRule(False, "allowlist", frozenset(), "top") mock_load.return_value = Mock() @@ -184,9 +184,9 @@ class TestWikiReverseLookup(unittest.TestCase): def _run(self, coro): return asyncio.get_event_loop().run_until_complete(coro) - @patch("gateway.platforms.feishu_comment._exec_request") + @patch("plugins.platforms.feishu.feishu_comment._exec_request") def test_reverse_lookup_success(self, mock_exec): - from gateway.platforms.feishu_comment import _reverse_lookup_wiki_token + from plugins.platforms.feishu.feishu_comment import _reverse_lookup_wiki_token mock_exec.return_value = (0, "Success", { "node": {"node_token": "WIKI_TOKEN_123", "obj_token": "docx_abc"}, @@ -200,37 +200,37 @@ class TestWikiReverseLookup(unittest.TestCase): self.assertEqual(query_dict["token"], "docx_abc") self.assertEqual(query_dict["obj_type"], "docx") - @patch("gateway.platforms.feishu_comment._exec_request") + @patch("plugins.platforms.feishu.feishu_comment._exec_request") def test_reverse_lookup_not_wiki(self, mock_exec): - from gateway.platforms.feishu_comment import _reverse_lookup_wiki_token + from plugins.platforms.feishu.feishu_comment import _reverse_lookup_wiki_token mock_exec.return_value = (131001, "not found", {}) result = self._run(_reverse_lookup_wiki_token(Mock(), "docx", "docx_abc")) self.assertIsNone(result) - @patch("gateway.platforms.feishu_comment._exec_request") + @patch("plugins.platforms.feishu.feishu_comment._exec_request") def test_reverse_lookup_service_error(self, mock_exec): - from gateway.platforms.feishu_comment import _reverse_lookup_wiki_token + from plugins.platforms.feishu.feishu_comment import _reverse_lookup_wiki_token mock_exec.return_value = (500, "internal error", {}) result = self._run(_reverse_lookup_wiki_token(Mock(), "docx", "docx_abc")) self.assertIsNone(result) - @patch("gateway.platforms.feishu_comment._reverse_lookup_wiki_token", new_callable=AsyncMock) - @patch("gateway.platforms.feishu_comment_rules.has_wiki_keys", return_value=True) - @patch("gateway.platforms.feishu_comment_rules.is_user_allowed", return_value=True) - @patch("gateway.platforms.feishu_comment_rules.resolve_rule") - @patch("gateway.platforms.feishu_comment_rules.load_config") - @patch("gateway.platforms.feishu_comment.add_comment_reaction", new_callable=AsyncMock) - @patch("gateway.platforms.feishu_comment.batch_query_comment", new_callable=AsyncMock) - @patch("gateway.platforms.feishu_comment.query_document_meta", new_callable=AsyncMock) + @patch("plugins.platforms.feishu.feishu_comment._reverse_lookup_wiki_token", new_callable=AsyncMock) + @patch("plugins.platforms.feishu.feishu_comment_rules.has_wiki_keys", return_value=True) + @patch("plugins.platforms.feishu.feishu_comment_rules.is_user_allowed", return_value=True) + @patch("plugins.platforms.feishu.feishu_comment_rules.resolve_rule") + @patch("plugins.platforms.feishu.feishu_comment_rules.load_config") + @patch("plugins.platforms.feishu.feishu_comment.add_comment_reaction", new_callable=AsyncMock) + @patch("plugins.platforms.feishu.feishu_comment.batch_query_comment", new_callable=AsyncMock) + @patch("plugins.platforms.feishu.feishu_comment.query_document_meta", new_callable=AsyncMock) def test_wiki_lookup_triggered_when_no_exact_match( self, mock_meta, mock_batch, mock_reaction, mock_load, mock_resolve, mock_allowed, mock_wiki_keys, mock_lookup, ): """Wiki reverse lookup should fire when rule falls to wildcard/top and wiki keys exist.""" - from gateway.platforms.feishu_comment import handle_drive_comment_event - from gateway.platforms.feishu_comment_rules import ResolvedCommentRule + from plugins.platforms.feishu.feishu_comment import handle_drive_comment_event + from plugins.platforms.feishu.feishu_comment_rules import ResolvedCommentRule # First resolve returns wildcard (no exact match), second returns exact wiki match mock_resolve.side_effect = [ diff --git a/tests/gateway/test_feishu_comment_rules.py b/tests/gateway/test_feishu_comment_rules.py index baef7a54744e..1ecff5ae9d43 100644 --- a/tests/gateway/test_feishu_comment_rules.py +++ b/tests/gateway/test_feishu_comment_rules.py @@ -8,7 +8,7 @@ import unittest from pathlib import Path from unittest.mock import patch -from gateway.platforms.feishu_comment_rules import ( +from plugins.platforms.feishu.feishu_comment_rules import ( CommentsConfig, CommentDocumentRule, ResolvedCommentRule, @@ -195,7 +195,7 @@ class TestIsUserAllowed(unittest.TestCase): def test_pairing_checks_store(self): rule = ResolvedCommentRule(True, "pairing", frozenset(), "top") with patch( - "gateway.platforms.feishu_comment_rules._load_pairing_approved", + "plugins.platforms.feishu.feishu_comment_rules._load_pairing_approved", return_value={"ou_approved"}, ): self.assertTrue(is_user_allowed(rule, "ou_approved")) @@ -256,8 +256,8 @@ class TestLoadConfig(unittest.TestCase): json.dump(raw, f) path = Path(f.name) try: - with patch("gateway.platforms.feishu_comment_rules.RULES_FILE", path): - with patch("gateway.platforms.feishu_comment_rules._rules_cache", _MtimeCache(path)): + with patch("plugins.platforms.feishu.feishu_comment_rules.RULES_FILE", path): + with patch("plugins.platforms.feishu.feishu_comment_rules._rules_cache", _MtimeCache(path)): cfg = load_config() self.assertTrue(cfg.enabled) self.assertEqual(cfg.policy, "allowlist") @@ -269,7 +269,7 @@ class TestLoadConfig(unittest.TestCase): path.unlink() def test_load_missing_file_returns_defaults(self): - with patch("gateway.platforms.feishu_comment_rules._rules_cache", _MtimeCache(Path("/nonexistent"))): + with patch("plugins.platforms.feishu.feishu_comment_rules._rules_cache", _MtimeCache(Path("/nonexistent"))): cfg = load_config() self.assertTrue(cfg.enabled) self.assertEqual(cfg.policy, "pairing") @@ -283,9 +283,9 @@ class TestPairingStore(unittest.TestCase): self._pairing_file = Path(self._tmpdir) / "pairing.json" with open(self._pairing_file, "w") as f: json.dump({"approved": {}}, f) - self._patcher_file = patch("gateway.platforms.feishu_comment_rules.PAIRING_FILE", self._pairing_file) + self._patcher_file = patch("plugins.platforms.feishu.feishu_comment_rules.PAIRING_FILE", self._pairing_file) self._patcher_cache = patch( - "gateway.platforms.feishu_comment_rules._pairing_cache", + "plugins.platforms.feishu.feishu_comment_rules._pairing_cache", _MtimeCache(self._pairing_file), ) self._patcher_file.start() diff --git a/tests/gateway/test_feishu_meeting_invite.py b/tests/gateway/test_feishu_meeting_invite.py index f8da38df6cba..e891ddf0a86e 100644 --- a/tests/gateway/test_feishu_meeting_invite.py +++ b/tests/gateway/test_feishu_meeting_invite.py @@ -6,7 +6,7 @@ from types import SimpleNamespace from unittest.mock import patch from gateway.platforms.base import MessageEvent -from gateway.platforms.feishu_meeting_invite import ( +from plugins.platforms.feishu.feishu_meeting_invite import ( build_meeting_invite_prompt, handle_meeting_invited_event, parse_meeting_invited_event, @@ -212,7 +212,7 @@ class TestMeetingInviteSendRouting(unittest.TestCase): def test_feishu_user_id_prefix_sends_with_user_id_receive_type(self): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter created_requests = [] diff --git a/tests/gateway/test_feishu_onboard.py b/tests/gateway/test_feishu_onboard.py index 80a9c826031b..72356cb1c32c 100644 --- a/tests/gateway/test_feishu_onboard.py +++ b/tests/gateway/test_feishu_onboard.py @@ -1,4 +1,4 @@ -"""Tests for gateway.platforms.feishu — Feishu scan-to-create registration.""" +"""Tests for plugins.platforms.feishu.adapter — Feishu scan-to-create registration.""" import json from unittest.mock import patch, MagicMock @@ -18,18 +18,18 @@ def _mock_urlopen(response_data, status=200): class TestPostRegistration: """Tests for the low-level HTTP helper.""" - @patch("gateway.platforms.feishu.urlopen") + @patch("plugins.platforms.feishu.adapter.urlopen") def test_post_registration_returns_parsed_json(self, mock_urlopen_fn): - from gateway.platforms.feishu import _post_registration + from plugins.platforms.feishu.adapter import _post_registration mock_urlopen_fn.return_value = _mock_urlopen({"nonce": "abc", "supported_auth_methods": ["client_secret"]}) result = _post_registration("https://accounts.feishu.cn", {"action": "init"}) assert result["nonce"] == "abc" assert "client_secret" in result["supported_auth_methods"] - @patch("gateway.platforms.feishu.urlopen") + @patch("plugins.platforms.feishu.adapter.urlopen") def test_post_registration_sends_form_encoded_body(self, mock_urlopen_fn): - from gateway.platforms.feishu import _post_registration + from plugins.platforms.feishu.adapter import _post_registration mock_urlopen_fn.return_value = _mock_urlopen({}) _post_registration("https://accounts.feishu.cn", {"action": "init", "key": "val"}) @@ -44,9 +44,9 @@ class TestPostRegistration: class TestInitRegistration: """Tests for the init step.""" - @patch("gateway.platforms.feishu.urlopen") + @patch("plugins.platforms.feishu.adapter.urlopen") def test_init_succeeds_when_client_secret_supported(self, mock_urlopen_fn): - from gateway.platforms.feishu import _init_registration + from plugins.platforms.feishu.adapter import _init_registration mock_urlopen_fn.return_value = _mock_urlopen({ "nonce": "abc", @@ -54,9 +54,9 @@ class TestInitRegistration: }) _init_registration("feishu") - @patch("gateway.platforms.feishu.urlopen") + @patch("plugins.platforms.feishu.adapter.urlopen") def test_init_raises_when_client_secret_not_supported(self, mock_urlopen_fn): - from gateway.platforms.feishu import _init_registration + from plugins.platforms.feishu.adapter import _init_registration mock_urlopen_fn.return_value = _mock_urlopen({ "nonce": "abc", @@ -65,9 +65,9 @@ class TestInitRegistration: with pytest.raises(RuntimeError, match="client_secret"): _init_registration("feishu") - @patch("gateway.platforms.feishu.urlopen") + @patch("plugins.platforms.feishu.adapter.urlopen") def test_init_uses_lark_url_for_lark_domain(self, mock_urlopen_fn): - from gateway.platforms.feishu import _init_registration + from plugins.platforms.feishu.adapter import _init_registration mock_urlopen_fn.return_value = _mock_urlopen({ "nonce": "abc", @@ -82,9 +82,9 @@ class TestInitRegistration: class TestBeginRegistration: """Tests for the begin step.""" - @patch("gateway.platforms.feishu.urlopen") + @patch("plugins.platforms.feishu.adapter.urlopen") def test_begin_returns_device_code_and_qr_url(self, mock_urlopen_fn): - from gateway.platforms.feishu import _begin_registration + from plugins.platforms.feishu.adapter import _begin_registration mock_urlopen_fn.return_value = _mock_urlopen({ "device_code": "dc_123", @@ -101,9 +101,9 @@ class TestBeginRegistration: assert result["interval"] == 5 assert result["expire_in"] == 600 - @patch("gateway.platforms.feishu.urlopen") + @patch("plugins.platforms.feishu.adapter.urlopen") def test_begin_sends_correct_archetype(self, mock_urlopen_fn): - from gateway.platforms.feishu import _begin_registration + from plugins.platforms.feishu.adapter import _begin_registration mock_urlopen_fn.return_value = _mock_urlopen({ "device_code": "dc_123", @@ -122,10 +122,10 @@ class TestBeginRegistration: class TestPollRegistration: """Tests for the poll step.""" - @patch("gateway.platforms.feishu.time") - @patch("gateway.platforms.feishu.urlopen") + @patch("plugins.platforms.feishu.adapter.time") + @patch("plugins.platforms.feishu.adapter.urlopen") def test_poll_returns_credentials_on_success(self, mock_urlopen_fn, mock_time): - from gateway.platforms.feishu import _poll_registration + from plugins.platforms.feishu.adapter import _poll_registration mock_time.monotonic.side_effect = [0, 1] mock_time.sleep = MagicMock() @@ -144,10 +144,10 @@ class TestPollRegistration: assert result["domain"] == "feishu" assert result["open_id"] == "ou_owner" - @patch("gateway.platforms.feishu.time") - @patch("gateway.platforms.feishu.urlopen") + @patch("plugins.platforms.feishu.adapter.time") + @patch("plugins.platforms.feishu.adapter.urlopen") def test_poll_switches_domain_on_lark_tenant_brand(self, mock_urlopen_fn, mock_time): - from gateway.platforms.feishu import _poll_registration + from plugins.platforms.feishu.adapter import _poll_registration mock_time.monotonic.side_effect = [0, 1, 2] mock_time.sleep = MagicMock() @@ -169,11 +169,11 @@ class TestPollRegistration: assert result is not None assert result["domain"] == "lark" - @patch("gateway.platforms.feishu.time") - @patch("gateway.platforms.feishu.urlopen") + @patch("plugins.platforms.feishu.adapter.time") + @patch("plugins.platforms.feishu.adapter.urlopen") def test_poll_success_with_lark_brand_in_same_response(self, mock_urlopen_fn, mock_time): """Credentials and lark tenant_brand in one response must not be discarded.""" - from gateway.platforms.feishu import _poll_registration + from plugins.platforms.feishu.adapter import _poll_registration mock_time.monotonic.side_effect = [0, 1] mock_time.sleep = MagicMock() @@ -191,10 +191,10 @@ class TestPollRegistration: assert result["domain"] == "lark" assert result["open_id"] == "ou_lark_direct" - @patch("gateway.platforms.feishu.time") - @patch("gateway.platforms.feishu.urlopen") + @patch("plugins.platforms.feishu.adapter.time") + @patch("plugins.platforms.feishu.adapter.urlopen") def test_poll_returns_none_on_access_denied(self, mock_urlopen_fn, mock_time): - from gateway.platforms.feishu import _poll_registration + from plugins.platforms.feishu.adapter import _poll_registration mock_time.monotonic.side_effect = [0, 1] mock_time.sleep = MagicMock() @@ -207,10 +207,10 @@ class TestPollRegistration: ) assert result is None - @patch("gateway.platforms.feishu.time") - @patch("gateway.platforms.feishu.urlopen") + @patch("plugins.platforms.feishu.adapter.time") + @patch("plugins.platforms.feishu.adapter.urlopen") def test_poll_returns_none_on_timeout(self, mock_urlopen_fn, mock_time): - from gateway.platforms.feishu import _poll_registration + from plugins.platforms.feishu.adapter import _poll_registration mock_time.monotonic.side_effect = [0, 999] mock_time.sleep = MagicMock() @@ -223,10 +223,10 @@ class TestPollRegistration: ) assert result is None - @patch("gateway.platforms.feishu.time") - @patch("gateway.platforms.feishu.urlopen") + @patch("plugins.platforms.feishu.adapter.time") + @patch("plugins.platforms.feishu.adapter.urlopen") def test_poll_timeout_uses_monotonic_clock(self, mock_urlopen_fn, mock_time): - from gateway.platforms.feishu import _poll_registration + from plugins.platforms.feishu.adapter import _poll_registration mock_time.monotonic.side_effect = [1000, 1000.2, 1001.1] mock_time.time.side_effect = [1000, 900, 901, 902] @@ -246,9 +246,9 @@ class TestPollRegistration: class TestRenderQr: """Tests for QR code terminal rendering.""" - @patch("gateway.platforms.feishu._qrcode_mod", create=True) + @patch("plugins.platforms.feishu.adapter._qrcode_mod", create=True) def test_render_qr_returns_true_on_success(self, mock_qrcode_mod): - from gateway.platforms.feishu import _render_qr + from plugins.platforms.feishu.adapter import _render_qr mock_qr = MagicMock() mock_qrcode_mod.QRCode.return_value = mock_qr @@ -258,20 +258,20 @@ class TestRenderQr: mock_qr.print_ascii.assert_called_once() def test_render_qr_returns_false_when_qrcode_missing(self): - from gateway.platforms.feishu import _render_qr + from plugins.platforms.feishu.adapter import _render_qr - with patch("gateway.platforms.feishu._qrcode_mod", None): + with patch("plugins.platforms.feishu.adapter._qrcode_mod", None): assert _render_qr("https://example.com/qr") is False class TestProbeBot: """Tests for bot connectivity verification.""" - @patch("gateway.platforms.feishu.FEISHU_AVAILABLE", True) + @patch("plugins.platforms.feishu.adapter.FEISHU_AVAILABLE", True) def test_probe_returns_bot_info_on_success(self): - from gateway.platforms.feishu import probe_bot + from plugins.platforms.feishu.adapter import probe_bot - with patch("gateway.platforms.feishu._probe_bot_sdk") as mock_sdk: + with patch("plugins.platforms.feishu.adapter._probe_bot_sdk") as mock_sdk: mock_sdk.return_value = {"bot_name": "TestBot", "bot_open_id": "ou_bot123"} result = probe_bot("cli_app", "secret", "feishu") @@ -279,21 +279,21 @@ class TestProbeBot: assert result["bot_name"] == "TestBot" assert result["bot_open_id"] == "ou_bot123" - @patch("gateway.platforms.feishu.FEISHU_AVAILABLE", True) + @patch("plugins.platforms.feishu.adapter.FEISHU_AVAILABLE", True) def test_probe_returns_none_on_failure(self): - from gateway.platforms.feishu import probe_bot + from plugins.platforms.feishu.adapter import probe_bot - with patch("gateway.platforms.feishu._probe_bot_sdk") as mock_sdk: + with patch("plugins.platforms.feishu.adapter._probe_bot_sdk") as mock_sdk: mock_sdk.return_value = None result = probe_bot("bad_id", "bad_secret", "feishu") assert result is None - @patch("gateway.platforms.feishu.FEISHU_AVAILABLE", False) - @patch("gateway.platforms.feishu.urlopen") + @patch("plugins.platforms.feishu.adapter.FEISHU_AVAILABLE", False) + @patch("plugins.platforms.feishu.adapter.urlopen") def test_http_fallback_when_sdk_unavailable(self, mock_urlopen_fn): """Without lark_oapi, probe falls back to raw HTTP.""" - from gateway.platforms.feishu import probe_bot + from plugins.platforms.feishu.adapter import probe_bot token_resp = _mock_urlopen({"code": 0, "tenant_access_token": "t-123"}) bot_resp = _mock_urlopen({"code": 0, "bot": {"bot_name": "HttpBot", "open_id": "ou_http"}}) @@ -303,10 +303,10 @@ class TestProbeBot: assert result is not None assert result["bot_name"] == "HttpBot" - @patch("gateway.platforms.feishu.FEISHU_AVAILABLE", False) - @patch("gateway.platforms.feishu.urlopen") + @patch("plugins.platforms.feishu.adapter.FEISHU_AVAILABLE", False) + @patch("plugins.platforms.feishu.adapter.urlopen") def test_http_fallback_returns_none_on_network_error(self, mock_urlopen_fn): - from gateway.platforms.feishu import probe_bot + from plugins.platforms.feishu.adapter import probe_bot from urllib.error import URLError mock_urlopen_fn.side_effect = URLError("connection refused") @@ -317,15 +317,15 @@ class TestProbeBot: class TestQrRegister: """Tests for the public qr_register entry point.""" - @patch("gateway.platforms.feishu.probe_bot") - @patch("gateway.platforms.feishu._render_qr") - @patch("gateway.platforms.feishu._poll_registration") - @patch("gateway.platforms.feishu._begin_registration") - @patch("gateway.platforms.feishu._init_registration") + @patch("plugins.platforms.feishu.adapter.probe_bot") + @patch("plugins.platforms.feishu.adapter._render_qr") + @patch("plugins.platforms.feishu.adapter._poll_registration") + @patch("plugins.platforms.feishu.adapter._begin_registration") + @patch("plugins.platforms.feishu.adapter._init_registration") def test_qr_register_success_flow( self, mock_init, mock_begin, mock_poll, mock_render, mock_probe ): - from gateway.platforms.feishu import qr_register + from plugins.platforms.feishu.adapter import qr_register mock_begin.return_value = { "device_code": "dc_123", @@ -350,22 +350,22 @@ class TestQrRegister: mock_init.assert_called_once() mock_render.assert_called_once() - @patch("gateway.platforms.feishu._init_registration") + @patch("plugins.platforms.feishu.adapter._init_registration") def test_qr_register_returns_none_on_init_failure(self, mock_init): - from gateway.platforms.feishu import qr_register + from plugins.platforms.feishu.adapter import qr_register mock_init.side_effect = RuntimeError("not supported") result = qr_register() assert result is None - @patch("gateway.platforms.feishu._render_qr") - @patch("gateway.platforms.feishu._poll_registration") - @patch("gateway.platforms.feishu._begin_registration") - @patch("gateway.platforms.feishu._init_registration") + @patch("plugins.platforms.feishu.adapter._render_qr") + @patch("plugins.platforms.feishu.adapter._poll_registration") + @patch("plugins.platforms.feishu.adapter._begin_registration") + @patch("plugins.platforms.feishu.adapter._init_registration") def test_qr_register_returns_none_on_poll_failure( self, mock_init, mock_begin, mock_poll, mock_render ): - from gateway.platforms.feishu import qr_register + from plugins.platforms.feishu.adapter import qr_register mock_begin.return_value = { "device_code": "dc_123", @@ -381,29 +381,29 @@ class TestQrRegister: # -- Contract: expected errors → None, unexpected errors → propagate -- - @patch("gateway.platforms.feishu._init_registration") + @patch("plugins.platforms.feishu.adapter._init_registration") def test_qr_register_returns_none_on_network_error(self, mock_init): """URLError (network down) is an expected failure → None.""" - from gateway.platforms.feishu import qr_register + from plugins.platforms.feishu.adapter import qr_register from urllib.error import URLError mock_init.side_effect = URLError("DNS resolution failed") result = qr_register() assert result is None - @patch("gateway.platforms.feishu._init_registration") + @patch("plugins.platforms.feishu.adapter._init_registration") def test_qr_register_returns_none_on_json_error(self, mock_init): """Malformed server response is an expected failure → None.""" - from gateway.platforms.feishu import qr_register + from plugins.platforms.feishu.adapter import qr_register mock_init.side_effect = json.JSONDecodeError("bad json", "", 0) result = qr_register() assert result is None - @patch("gateway.platforms.feishu._init_registration") + @patch("plugins.platforms.feishu.adapter._init_registration") def test_qr_register_propagates_unexpected_errors(self, mock_init): """Bugs (e.g. AttributeError) must not be swallowed — they propagate.""" - from gateway.platforms.feishu import qr_register + from plugins.platforms.feishu.adapter import qr_register mock_init.side_effect = AttributeError("some internal bug") with pytest.raises(AttributeError, match="some internal bug"): @@ -411,29 +411,29 @@ class TestQrRegister: # -- Negative paths: partial/malformed server responses -- - @patch("gateway.platforms.feishu._render_qr") - @patch("gateway.platforms.feishu._begin_registration") - @patch("gateway.platforms.feishu._init_registration") + @patch("plugins.platforms.feishu.adapter._render_qr") + @patch("plugins.platforms.feishu.adapter._begin_registration") + @patch("plugins.platforms.feishu.adapter._init_registration") def test_qr_register_returns_none_when_begin_missing_device_code( self, mock_init, mock_begin, mock_render ): """Server returns begin response without device_code → RuntimeError → None.""" - from gateway.platforms.feishu import qr_register + from plugins.platforms.feishu.adapter import qr_register mock_begin.side_effect = RuntimeError("Feishu registration did not return a device_code") result = qr_register() assert result is None - @patch("gateway.platforms.feishu.probe_bot") - @patch("gateway.platforms.feishu._render_qr") - @patch("gateway.platforms.feishu._poll_registration") - @patch("gateway.platforms.feishu._begin_registration") - @patch("gateway.platforms.feishu._init_registration") + @patch("plugins.platforms.feishu.adapter.probe_bot") + @patch("plugins.platforms.feishu.adapter._render_qr") + @patch("plugins.platforms.feishu.adapter._poll_registration") + @patch("plugins.platforms.feishu.adapter._begin_registration") + @patch("plugins.platforms.feishu.adapter._init_registration") def test_qr_register_succeeds_even_when_probe_fails( self, mock_init, mock_begin, mock_poll, mock_render, mock_probe ): """Registration succeeds but probe fails → result with bot_name=None.""" - from gateway.platforms.feishu import qr_register + from plugins.platforms.feishu.adapter import qr_register mock_begin.return_value = { "device_code": "dc_123", diff --git a/tests/gateway/test_matrix.py b/tests/gateway/test_matrix.py index 00b100d5a89e..e31286b32321 100644 --- a/tests/gateway/test_matrix.py +++ b/tests/gateway/test_matrix.py @@ -341,7 +341,7 @@ class TestMatrixConfigLoading: def _make_adapter(): """Create a MatrixAdapter with mocked config.""" - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter config = PlatformConfig( enabled=True, token="syt_test_token", @@ -367,7 +367,7 @@ class TestMatrixTypingIndicator: @pytest.mark.asyncio async def test_stop_typing_clears_matrix_typing_state(self): """stop_typing() should send typing=false instead of waiting for timeout expiry.""" - from gateway.platforms.matrix import RoomID + from plugins.platforms.matrix.adapter import RoomID await self.adapter.stop_typing("!room:example.org") @@ -592,7 +592,7 @@ class TestMatrixBangCommandAlias: return captured_event def test_known_bang_command_normalizes_to_slash_command(self): - from gateway.platforms.matrix import _normalize_matrix_bang_command + from plugins.platforms.matrix.adapter import _normalize_matrix_bang_command assert _normalize_matrix_bang_command("!model") == "/model" assert ( @@ -606,7 +606,7 @@ class TestMatrixBangCommandAlias: assert _normalize_matrix_bang_command("!tasks") == "/tasks" def test_unknown_bang_text_is_not_treated_as_command(self): - from gateway.platforms.matrix import _normalize_matrix_bang_command + from plugins.platforms.matrix.adapter import _normalize_matrix_bang_command assert _normalize_matrix_bang_command("!important note") == "!important note" assert _normalize_matrix_bang_command("! wow") == "! wow" @@ -666,7 +666,7 @@ class TestMatrixBangCommandAlias: def test_bang_alias_underscore_resolves_to_hyphen_form(self): """!set_home must emit a dispatchable token even though set_home is not itself registered — the hyphenated alias set-home is.""" - from gateway.platforms.matrix import _normalize_matrix_bang_command + from plugins.platforms.matrix.adapter import _normalize_matrix_bang_command # set_home (underscore) is NOT a registered command/alias, but # set-home (hyphen) is. The normalizer must emit the resolvable form. @@ -686,7 +686,7 @@ class TestMatrixBangCommandAlias: with patch.object( skill_commands_mod, "get_skill_commands", return_value=fake_skills ): - from gateway.platforms.matrix import _normalize_matrix_bang_command + from plugins.platforms.matrix.adapter import _normalize_matrix_bang_command # is_gateway_known_command won't know these; the skill branch must. assert _normalize_matrix_bang_command("!arxiv") == "/arxiv" @@ -886,7 +886,7 @@ class TestMatrixDisplayName: class TestMatrixModuleImport: def test_module_importable_without_mautrix(self): - """gateway.platforms.matrix must be importable even when mautrix is + """plugins.platforms.matrix.adapter must be importable even when mautrix is not installed — otherwise the gateway crashes for ALL platforms. This test uses a subprocess to avoid polluting the current process's @@ -908,7 +908,7 @@ class TestMatrixModuleImport: "for k in list(sys.modules):\n" " if k.startswith('mautrix'): del sys.modules[k]\n" "from unittest.mock import patch\n" - "from gateway.platforms.matrix import check_matrix_requirements\n" + "from plugins.platforms.matrix.adapter import check_matrix_requirements\n" "with patch('tools.lazy_deps.ensure', side_effect=ImportError('blocked')):\n" " assert not check_matrix_requirements()\n" "print('OK')\n" @@ -925,7 +925,7 @@ class TestMatrixRequirements: monkeypatch.setenv("MATRIX_ACCESS_TOKEN", "syt_test") monkeypatch.setenv("MATRIX_HOMESERVER", "https://matrix.example.org") monkeypatch.delenv("MATRIX_ENCRYPTION", raising=False) - from gateway.platforms.matrix import check_matrix_requirements + from plugins.platforms.matrix.adapter import check_matrix_requirements try: import mautrix # noqa: F401 assert check_matrix_requirements() is True @@ -937,13 +937,13 @@ class TestMatrixRequirements: monkeypatch.delenv("MATRIX_ACCESS_TOKEN", raising=False) monkeypatch.delenv("MATRIX_PASSWORD", raising=False) monkeypatch.delenv("MATRIX_HOMESERVER", raising=False) - from gateway.platforms.matrix import check_matrix_requirements + from plugins.platforms.matrix.adapter import check_matrix_requirements assert check_matrix_requirements() is False def test_check_requirements_without_homeserver(self, monkeypatch): monkeypatch.setenv("MATRIX_ACCESS_TOKEN", "syt_test") monkeypatch.delenv("MATRIX_HOMESERVER", raising=False) - from gateway.platforms.matrix import check_matrix_requirements + from plugins.platforms.matrix.adapter import check_matrix_requirements assert check_matrix_requirements() is False def test_check_requirements_encryption_true_no_e2ee_deps(self, monkeypatch): @@ -952,7 +952,7 @@ class TestMatrixRequirements: monkeypatch.setenv("MATRIX_HOMESERVER", "https://matrix.example.org") monkeypatch.setenv("MATRIX_ENCRYPTION", "true") - from gateway.platforms import matrix as matrix_mod + import plugins.platforms.matrix.adapter as matrix_mod with patch.object(matrix_mod, "_check_e2ee_deps", return_value=False), \ patch("tools.lazy_deps.ensure", side_effect=ImportError("mautrix unavailable")): assert matrix_mod.check_matrix_requirements() is False @@ -963,7 +963,7 @@ class TestMatrixRequirements: monkeypatch.setenv("MATRIX_HOMESERVER", "https://matrix.example.org") monkeypatch.delenv("MATRIX_ENCRYPTION", raising=False) - from gateway.platforms import matrix as matrix_mod + import plugins.platforms.matrix.adapter as matrix_mod with patch.object(matrix_mod, "_check_e2ee_deps", return_value=False): # Still needs mautrix itself to be importable try: @@ -979,7 +979,7 @@ class TestMatrixRequirements: monkeypatch.setenv("MATRIX_HOMESERVER", "https://matrix.example.org") monkeypatch.setenv("MATRIX_ENCRYPTION", "true") - from gateway.platforms import matrix as matrix_mod + import plugins.platforms.matrix.adapter as matrix_mod with patch.object(matrix_mod, "_check_e2ee_deps", return_value=True): try: import mautrix # noqa: F401 @@ -998,7 +998,7 @@ class TestMatrixRequirements: a confusing ``No module named 'asyncpg'`` deep in ``MatrixAdapter.connect()``. """ - from gateway.platforms.matrix import _check_e2ee_deps + from plugins.platforms.matrix.adapter import _check_e2ee_deps import builtins real_import = builtins.__import__ @@ -1016,7 +1016,7 @@ class TestMatrixRequirements: Mautrix's ``Database.create("sqlite:///...")`` driver lookup imports aiosqlite lazily — without it, connect fails at ``crypto_db.start()``. """ - from gateway.platforms.matrix import _check_e2ee_deps + from plugins.platforms.matrix.adapter import _check_e2ee_deps import builtins real_import = builtins.__import__ @@ -1040,7 +1040,7 @@ class TestMatrixRequirements: monkeypatch.setenv("MATRIX_HOMESERVER", "https://matrix.example.org") monkeypatch.delenv("MATRIX_ENCRYPTION", raising=False) - from gateway.platforms import matrix as matrix_mod + import plugins.platforms.matrix.adapter as matrix_mod # Simulate "mautrix installed, asyncpg missing" → feature_missing # returns a non-empty tuple → ensure_and_bind MUST be called. @@ -1070,7 +1070,7 @@ class TestMatrixAccessTokenAuth: @pytest.mark.asyncio async def test_connect_with_access_token_and_encryption(self): """connect() should call whoami, set user_id/device_id, set up crypto.""" - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter config = PlatformConfig( enabled=True, @@ -1124,7 +1124,7 @@ class TestMatrixAccessTokenAuth: fake_mautrix_mods["mautrix.client"].Client = MagicMock(return_value=mock_client) fake_mautrix_mods["mautrix.crypto"].OlmMachine = MagicMock(return_value=mock_olm) - from gateway.platforms import matrix as matrix_mod + import plugins.platforms.matrix.adapter as matrix_mod with patch.object(matrix_mod, "_check_e2ee_deps", return_value=True): with patch.dict("sys.modules", fake_mautrix_mods): with patch.object(adapter, "_refresh_dm_cache", AsyncMock()): @@ -1176,7 +1176,7 @@ class TestMatrixE2EEHardFail: @pytest.mark.asyncio async def test_connect_fails_when_encryption_true_but_no_e2ee_deps(self): - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter config = PlatformConfig( enabled=True, @@ -1203,7 +1203,7 @@ class TestMatrixE2EEHardFail: fake_mautrix_mods["mautrix.client"].Client = MagicMock(return_value=mock_client) - from gateway.platforms import matrix as matrix_mod + import plugins.platforms.matrix.adapter as matrix_mod with patch.object(matrix_mod, "_check_e2ee_deps", return_value=False): with patch.dict("sys.modules", fake_mautrix_mods): with patch.object(adapter, "_sync_loop", AsyncMock(return_value=None)): @@ -1214,7 +1214,7 @@ class TestMatrixE2EEHardFail: @pytest.mark.asyncio async def test_connect_fails_when_crypto_setup_raises(self): """Even if _check_e2ee_deps passes, if OlmMachine raises, hard-fail.""" - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter config = PlatformConfig( enabled=True, @@ -1242,7 +1242,7 @@ class TestMatrixE2EEHardFail: fake_mautrix_mods["mautrix.client"].Client = MagicMock(return_value=mock_client) fake_mautrix_mods["mautrix.crypto"].OlmMachine = MagicMock(side_effect=Exception("olm init failed")) - from gateway.platforms import matrix as matrix_mod + import plugins.platforms.matrix.adapter as matrix_mod with patch.object(matrix_mod, "_check_e2ee_deps", return_value=True): with patch.dict("sys.modules", fake_mautrix_mods): result = await adapter.connect() @@ -1254,7 +1254,7 @@ class TestMatrixDeviceId: """MATRIX_DEVICE_ID should be used for stable device identity.""" def test_device_id_from_config_extra(self): - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter config = PlatformConfig( enabled=True, @@ -1270,7 +1270,7 @@ class TestMatrixDeviceId: def test_device_id_from_env(self, monkeypatch): monkeypatch.setenv("MATRIX_DEVICE_ID", "FROM_ENV") - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter config = PlatformConfig( enabled=True, @@ -1285,7 +1285,7 @@ class TestMatrixDeviceId: def test_device_id_config_takes_precedence_over_env(self, monkeypatch): monkeypatch.setenv("MATRIX_DEVICE_ID", "FROM_ENV") - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter config = PlatformConfig( enabled=True, @@ -1301,7 +1301,7 @@ class TestMatrixDeviceId: @pytest.mark.asyncio async def test_connect_uses_configured_device_id_over_whoami(self): """When MATRIX_DEVICE_ID is set, it should be used instead of whoami device_id.""" - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter config = PlatformConfig( enabled=True, @@ -1348,7 +1348,7 @@ class TestMatrixDeviceId: fake_mautrix_mods["mautrix.client"].Client = MagicMock(return_value=mock_client) fake_mautrix_mods["mautrix.crypto"].OlmMachine = MagicMock(return_value=mock_olm) - from gateway.platforms import matrix as matrix_mod + import plugins.platforms.matrix.adapter as matrix_mod with patch.object(matrix_mod, "_check_e2ee_deps", return_value=True): with patch.dict("sys.modules", fake_mautrix_mods): with patch.object(adapter, "_refresh_dm_cache", AsyncMock()): @@ -1367,7 +1367,7 @@ class TestMatrixPasswordLoginDeviceId: @pytest.mark.asyncio async def test_password_login_uses_device_id(self): - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter config = PlatformConfig( enabled=True, @@ -1591,7 +1591,7 @@ class TestMatrixEncryptedSendFallback: class TestJoinedRoomsReference: def test_joined_rooms_reference_preserved_after_reassignment(self): """_CryptoStateStore must see updates after initial sync populates rooms.""" - from gateway.platforms.matrix import _CryptoStateStore + from plugins.platforms.matrix.adapter import _CryptoStateStore joined = set() store = _CryptoStateStore(MagicMock(), joined) @@ -1612,7 +1612,7 @@ class TestJoinedRoomsReference: class TestMatrixEncryptedEventHandler: @pytest.mark.asyncio async def test_connect_registers_encrypted_event_handler_when_encryption_on(self): - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter config = PlatformConfig( enabled=True, @@ -1658,7 +1658,7 @@ class TestMatrixEncryptedEventHandler: fake_mautrix_mods["mautrix.client"].Client = MagicMock(return_value=mock_client) fake_mautrix_mods["mautrix.crypto"].OlmMachine = MagicMock(return_value=mock_olm) - from gateway.platforms import matrix as matrix_mod + import plugins.platforms.matrix.adapter as matrix_mod with patch.object(matrix_mod, "_check_e2ee_deps", return_value=True): with patch.dict("sys.modules", fake_mautrix_mods): with patch.object(adapter, "_refresh_dm_cache", AsyncMock()): @@ -1678,7 +1678,7 @@ class TestMatrixEncryptedEventHandler: @pytest.mark.asyncio async def test_connect_fails_on_stale_otk_conflict(self): """connect() must refuse E2EE when OTK upload hits 'already exists'.""" - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter config = PlatformConfig( enabled=True, @@ -1727,7 +1727,7 @@ class TestMatrixEncryptedEventHandler: fake_mautrix_mods["mautrix.client"].Client = MagicMock(return_value=mock_client) fake_mautrix_mods["mautrix.crypto"].OlmMachine = MagicMock(return_value=mock_olm) - from gateway.platforms import matrix as matrix_mod + import plugins.platforms.matrix.adapter as matrix_mod with patch.object(matrix_mod, "_check_e2ee_deps", return_value=True): with patch.dict("sys.modules", fake_mautrix_mods): result = await adapter.connect() @@ -1800,7 +1800,7 @@ class TestMatrixMarkdownHtmlSecurity: """Tests for HTML injection prevention in _markdown_to_html_fallback.""" def setup_method(self): - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter self.convert = MatrixAdapter._markdown_to_html_fallback def test_script_injection_in_header(self): @@ -1861,7 +1861,7 @@ class TestMatrixMarkdownHtmlFormatting: """Tests for new formatting capabilities in _markdown_to_html_fallback.""" def setup_method(self): - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter self.convert = MatrixAdapter._markdown_to_html_fallback def test_fenced_code_block(self): @@ -1928,23 +1928,23 @@ class TestMatrixMarkdownHtmlFormatting: class TestMatrixLinkSanitization: def test_safe_https_url(self): - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter assert MatrixAdapter._sanitize_link_url("https://example.com") == "https://example.com" def test_javascript_blocked(self): - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter assert MatrixAdapter._sanitize_link_url("javascript:alert(1)") == "" def test_data_blocked(self): - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter assert MatrixAdapter._sanitize_link_url("data:text/html,bad") == "" def test_vbscript_blocked(self): - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter assert MatrixAdapter._sanitize_link_url("vbscript:bad") == "" def test_quotes_escaped(self): - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter result = MatrixAdapter._sanitize_link_url('http://x"y') assert '"' not in result assert """ in result @@ -2561,7 +2561,7 @@ class TestMatrixClockSkewWarning: # Server events are dated 2h before startup_ts (skewed clock). skewed_event_ts_ms = int((self.adapter._startup_ts - 7200) * 1000) - with caplog.at_level(logging.WARNING, logger="gateway.platforms.matrix"): + with caplog.at_level(logging.WARNING, logger="plugins.platforms.matrix.adapter"): for i in range(5): ev = self._mk_event( sender=f"@alice{i}:example.org", ts_ms=skewed_event_ts_ms @@ -2575,7 +2575,7 @@ class TestMatrixClockSkewWarning: # assertion. skew_warnings = [ r for r in caplog.records - if r.name == "gateway.platforms.matrix" + if r.name == "plugins.platforms.matrix.adapter" and r.levelname == "WARNING" and "set-ntp" in r.getMessage() ] @@ -2600,7 +2600,7 @@ class TestMatrixClockSkewWarning: self.adapter._startup_ts = now - 1 old_ts_ms = int((self.adapter._startup_ts - 3600) * 1000) - with caplog.at_level(logging.WARNING, logger="gateway.platforms.matrix"): + with caplog.at_level(logging.WARNING, logger="plugins.platforms.matrix.adapter"): for i in range(5): ev = self._mk_event( sender=f"@alice{i}:example.org", ts_ms=old_ts_ms @@ -2611,7 +2611,7 @@ class TestMatrixClockSkewWarning: assert self.adapter._clock_skew_warned is False skew_warnings = [ r for r in caplog.records - if r.name == "gateway.platforms.matrix" + if r.name == "plugins.platforms.matrix.adapter" and "set-ntp" in r.getMessage() ] assert skew_warnings == [] @@ -2626,7 +2626,7 @@ class TestMatrixClockSkewWarning: self.adapter._startup_ts = now - 120 # extra slack vs the 30s gate old_ts_ms = int((self.adapter._startup_ts - 3600) * 1000) - with caplog.at_level(logging.WARNING, logger="gateway.platforms.matrix"): + with caplog.at_level(logging.WARNING, logger="plugins.platforms.matrix.adapter"): for i in range(2): # only 2 late drops — under the threshold ev = self._mk_event( sender=f"@alice{i}:example.org", ts_ms=old_ts_ms @@ -2652,7 +2652,7 @@ class TestMatrixClockSkewWarning: self.adapter._startup_ts = now - 120 # Each event has a different age, ranging from 1h to 30d ago. ages_in_hours = [1, 24, 168, 720, 4] # 1h, 1d, 1w, 30d, 4h - with caplog.at_level(logging.WARNING, logger="gateway.platforms.matrix"): + with caplog.at_level(logging.WARNING, logger="plugins.platforms.matrix.adapter"): for i, hrs in enumerate(ages_in_hours): ts_ms = int((self.adapter._startup_ts - hrs * 3600) * 1000) ev = self._mk_event( @@ -2665,7 +2665,7 @@ class TestMatrixClockSkewWarning: assert self.adapter._clock_skew_warned is False skew_warnings = [ r for r in caplog.records - if r.name == "gateway.platforms.matrix" + if r.name == "plugins.platforms.matrix.adapter" and "set-ntp" in r.getMessage() ] assert skew_warnings == [] @@ -2689,7 +2689,7 @@ class TestMatrixClockSkewWarning: self.adapter._startup_ts = now - 60 skewed_ms = int((self.adapter._startup_ts - 7200) * 1000) - with caplog.at_level(logging.WARNING, logger="gateway.platforms.matrix"): + with caplog.at_level(logging.WARNING, logger="plugins.platforms.matrix.adapter"): for i in range(3): ev = self._mk_event( sender=f"@alice{i}:example.org", ts_ms=skewed_ms, @@ -2715,7 +2715,7 @@ class TestMatrixClockSkewWarning: skew_warnings = [ r for r in caplog.records - if r.name == "gateway.platforms.matrix" + if r.name == "plugins.platforms.matrix.adapter" and "set-ntp" in r.getMessage() ] assert len(skew_warnings) == 2, ( @@ -2792,7 +2792,7 @@ class TestMatrixProxyConfig: for k, v in proxy_env.items(): monkeypatch.setenv(k, v) with patch.dict("sys.modules", _make_fake_mautrix()): - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter cfg = PlatformConfig(enabled=True, token="syt_test", extra={"homeserver": "https://matrix.example.org", "user_id": "@bot:example.org"}) @@ -2825,7 +2825,7 @@ class TestCreateMatrixSession: @pytest.mark.asyncio async def test_no_proxy_returns_trust_env_session(self): with patch.dict("sys.modules", _make_fake_mautrix()): - from gateway.platforms.matrix import _create_matrix_session + from plugins.platforms.matrix.adapter import _create_matrix_session session = _create_matrix_session(None) try: assert session.trust_env is True @@ -2835,7 +2835,7 @@ class TestCreateMatrixSession: @pytest.mark.asyncio async def test_http_proxy_sets_default_proxy(self): with patch.dict("sys.modules", _make_fake_mautrix()): - from gateway.platforms.matrix import _create_matrix_session + from plugins.platforms.matrix.adapter import _create_matrix_session session = _create_matrix_session("http://proxy:8080") try: assert str(session._default_proxy) == "http://proxy:8080" @@ -2853,7 +2853,7 @@ class TestCreateMatrixSession: ) ), }): - from gateway.platforms.matrix import _create_matrix_session + from plugins.platforms.matrix.adapter import _create_matrix_session session = _create_matrix_session("socks5://proxy:1080") try: assert session.connector is fake_connector diff --git a/tests/gateway/test_matrix_approval_reaction_fail_closed.py b/tests/gateway/test_matrix_approval_reaction_fail_closed.py index c9b5277ee6a7..3b390a97561c 100644 --- a/tests/gateway/test_matrix_approval_reaction_fail_closed.py +++ b/tests/gateway/test_matrix_approval_reaction_fail_closed.py @@ -17,7 +17,7 @@ import pytest # --------------------------------------------------------------------------- -# Stub mautrix so gateway.platforms.matrix can be imported without the SDK. +# Stub mautrix so plugins.platforms.matrix.adapter can be imported without the SDK. # --------------------------------------------------------------------------- def _stub_mautrix(): @@ -39,7 +39,7 @@ def _stub_mautrix(): _stub_mautrix() -from gateway.platforms.matrix import MatrixAdapter, _MatrixApprovalPrompt # noqa: E402 +from plugins.platforms.matrix.adapter import MatrixAdapter, _MatrixApprovalPrompt # noqa: E402 # --------------------------------------------------------------------------- diff --git a/tests/gateway/test_matrix_exec_approval.py b/tests/gateway/test_matrix_exec_approval.py index a7afe912cba5..05477279695a 100644 --- a/tests/gateway/test_matrix_exec_approval.py +++ b/tests/gateway/test_matrix_exec_approval.py @@ -10,7 +10,7 @@ class TestMatrixExecApprovalReactions: @pytest.mark.asyncio async def test_send_exec_approval_registers_prompt_and_seeds_reactions(self, monkeypatch): monkeypatch.setenv("MATRIX_ALLOWED_USERS", "@liizfq:liizfq.top") - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter adapter = MatrixAdapter(PlatformConfig(enabled=True, token="tok", extra={"homeserver": "https://matrix.example.org"})) adapter._client = types.SimpleNamespace() @@ -34,7 +34,7 @@ class TestMatrixExecApprovalReactions: @pytest.mark.asyncio async def test_reaction_resolves_pending_approval(self, monkeypatch): monkeypatch.setenv("MATRIX_ALLOWED_USERS", "@liizfq:liizfq.top") - from gateway.platforms.matrix import MatrixAdapter, _MatrixApprovalPrompt + from plugins.platforms.matrix.adapter import MatrixAdapter, _MatrixApprovalPrompt adapter = MatrixAdapter(PlatformConfig(enabled=True, token="tok", extra={"homeserver": "https://matrix.example.org"})) # Resolve user_id so _is_self_sender doesn't defensively drop all traffic (#15763). diff --git a/tests/gateway/test_matrix_mention.py b/tests/gateway/test_matrix_mention.py index 634c1c765f96..a8691c0cb8b0 100644 --- a/tests/gateway/test_matrix_mention.py +++ b/tests/gateway/test_matrix_mention.py @@ -17,7 +17,7 @@ from gateway.config import PlatformConfig def _make_adapter(tmp_path=None): """Create a MatrixAdapter with mocked config.""" - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter config = PlatformConfig( enabled=True, diff --git a/tests/gateway/test_matrix_voice.py b/tests/gateway/test_matrix_voice.py index 51bf150b29b5..2e1cdc0befa5 100644 --- a/tests/gateway/test_matrix_voice.py +++ b/tests/gateway/test_matrix_voice.py @@ -27,7 +27,7 @@ from gateway.platforms.base import MessageType def _make_adapter(): """Create a MatrixAdapter with mocked config.""" - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter from gateway.config import PlatformConfig config = PlatformConfig( diff --git a/tests/gateway/test_platform_http_client_limits.py b/tests/gateway/test_platform_http_client_limits.py index 074a6d52ec3b..8f2ed7bc602a 100644 --- a/tests/gateway/test_platform_http_client_limits.py +++ b/tests/gateway/test_platform_http_client_limits.py @@ -78,7 +78,7 @@ def test_helper_is_importable_from_every_platform_that_uses_it(): # Just importing exercises the helper's import path for each adapter. import gateway.platforms.qqbot.adapter # noqa: F401 import gateway.platforms.wecom # noqa: F401 - import gateway.platforms.dingtalk # noqa: F401 + import plugins.platforms.dingtalk.adapter # noqa: F401 import gateway.platforms.signal # noqa: F401 import gateway.platforms.bluebubbles # noqa: F401 import gateway.platforms.wecom_callback # noqa: F401 @@ -98,7 +98,7 @@ class TestWhatsappTypingLeakFix: def test_bare_await_removed(self): import inspect - import gateway.platforms.whatsapp as mod + import plugins.platforms.whatsapp.adapter as mod src = inspect.getsource(mod.WhatsAppAdapter.send_typing) # The fix must be structural: the post() call is inside an diff --git a/tests/gateway/test_send_image_file.py b/tests/gateway/test_send_image_file.py index 9cbf48fd0d7c..1a70705a0423 100644 --- a/tests/gateway/test_send_image_file.py +++ b/tests/gateway/test_send_image_file.py @@ -82,7 +82,7 @@ def _ensure_telegram_mock(): _ensure_telegram_mock() -from gateway.platforms.telegram import TelegramAdapter # noqa: E402 +from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402 class TestTelegramSendImageFile: diff --git a/tests/gateway/test_send_multiple_images.py b/tests/gateway/test_send_multiple_images.py index 5fab55c4a700..d2be84e84864 100644 --- a/tests/gateway/test_send_multiple_images.py +++ b/tests/gateway/test_send_multiple_images.py @@ -115,7 +115,7 @@ def _ensure_telegram_mock(): _ensure_telegram_mock() -from gateway.platforms.telegram import TelegramAdapter # noqa: E402 +from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402 class TestTelegramMultiImage: diff --git a/tests/gateway/test_setup_feishu.py b/tests/gateway/test_setup_feishu.py index 26165528e24e..bd1d341ea734 100644 --- a/tests/gateway/test_setup_feishu.py +++ b/tests/gateway/test_setup_feishu.py @@ -39,20 +39,20 @@ def _run_setup_feishu( def mock_get(name): return existing_env.get(name, "") - with patch("hermes_cli.gateway.save_env_value", side_effect=mock_save), \ - patch("hermes_cli.gateway.get_env_value", side_effect=mock_get), \ - patch("hermes_cli.gateway.prompt_yes_no", side_effect=prompt_yes_no_responses), \ - patch("hermes_cli.gateway.prompt_choice", side_effect=prompt_choice_responses), \ - patch("hermes_cli.gateway.prompt", side_effect=prompt_responses), \ - patch("hermes_cli.gateway.print_info"), \ - patch("hermes_cli.gateway.print_success"), \ - patch("hermes_cli.gateway.print_warning"), \ - patch("hermes_cli.gateway.print_error"), \ - patch("hermes_cli.gateway.color", side_effect=lambda t, c: t), \ - patch("gateway.platforms.feishu.qr_register", return_value=qr_result): + with patch("hermes_cli.config.save_env_value", side_effect=mock_save), \ + patch("hermes_cli.config.get_env_value", side_effect=mock_get), \ + patch("hermes_cli.cli_output.prompt_yes_no", side_effect=prompt_yes_no_responses), \ + patch("hermes_cli.setup.prompt_choice", side_effect=prompt_choice_responses), \ + patch("hermes_cli.cli_output.prompt", side_effect=prompt_responses), \ + patch("hermes_cli.cli_output.print_header"), \ + patch("hermes_cli.cli_output.print_info"), \ + patch("hermes_cli.cli_output.print_success"), \ + patch("hermes_cli.cli_output.print_warning"), \ + patch("hermes_cli.cli_output.print_error"), \ + patch("plugins.platforms.feishu.adapter.qr_register", return_value=qr_result): - from hermes_cli.gateway import _setup_feishu - _setup_feishu() + from plugins.platforms.feishu.adapter import interactive_setup + interactive_setup() return saved_env @@ -120,7 +120,7 @@ class TestSetupFeishuConnectionMode: ) assert env["FEISHU_CONNECTION_MODE"] == "websocket" - @patch("gateway.platforms.feishu.probe_bot", return_value=None) + @patch("plugins.platforms.feishu.adapter.probe_bot", return_value=None) def test_manual_path_websocket(self, _mock_probe): env = _run_setup_feishu( qr_result=None, @@ -129,7 +129,7 @@ class TestSetupFeishuConnectionMode: ) assert env["FEISHU_CONNECTION_MODE"] == "websocket" - @patch("gateway.platforms.feishu.probe_bot", return_value=None) + @patch("plugins.platforms.feishu.adapter.probe_bot", return_value=None) def test_manual_path_webhook(self, _mock_probe): env = _run_setup_feishu( qr_result=None, @@ -248,7 +248,7 @@ class TestSetupFeishuAdapterIntegration: with patch.dict(os.environ, env, clear=True): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) assert adapter._app_id == "cli_test_app" assert adapter._app_secret == "test_secret_value" @@ -261,7 +261,7 @@ class TestSetupFeishuAdapterIntegration: env = self._make_env_from_setup(dm_idx=1) with patch.dict(os.environ, env, clear=True): - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter from gateway.config import PlatformConfig # Verify adapter initializes without error and env var is correct. FeishuAdapter(PlatformConfig()) @@ -274,6 +274,6 @@ class TestSetupFeishuAdapterIntegration: with patch.dict(os.environ, env, clear=True): from gateway.config import PlatformConfig - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter adapter = FeishuAdapter(PlatformConfig()) assert adapter._group_policy == "open" diff --git a/tests/gateway/test_stream_consumer.py b/tests/gateway/test_stream_consumer.py index 9a445532d0d9..f1421cf79896 100644 --- a/tests/gateway/test_stream_consumer.py +++ b/tests/gateway/test_stream_consumer.py @@ -148,14 +148,14 @@ class TestEditMessageFinalizeSignature: @pytest.mark.parametrize( "module_path,class_name", [ - ("gateway.platforms.telegram", "TelegramAdapter"), + ("plugins.platforms.telegram.adapter", "TelegramAdapter"), ("plugins.platforms.discord.adapter", "DiscordAdapter"), ("gateway.platforms.slack", "SlackAdapter"), - ("gateway.platforms.matrix", "MatrixAdapter"), + ("plugins.platforms.matrix.adapter", "MatrixAdapter"), ("plugins.platforms.mattermost.adapter", "MattermostAdapter"), - ("gateway.platforms.feishu", "FeishuAdapter"), - ("gateway.platforms.whatsapp", "WhatsAppAdapter"), - ("gateway.platforms.dingtalk", "DingTalkAdapter"), + ("plugins.platforms.feishu.adapter", "FeishuAdapter"), + ("plugins.platforms.whatsapp.adapter", "WhatsAppAdapter"), + ("plugins.platforms.dingtalk.adapter", "DingTalkAdapter"), ], ) def test_edit_message_accepts_finalize(self, module_path, class_name): diff --git a/tests/gateway/test_stream_consumer_fresh_final.py b/tests/gateway/test_stream_consumer_fresh_final.py index 2ecef4a488b4..c834f95b9c6d 100644 --- a/tests/gateway/test_stream_consumer_fresh_final.py +++ b/tests/gateway/test_stream_consumer_fresh_final.py @@ -391,7 +391,7 @@ class TestTelegramAdapterDeleteMessage: """Contract: Telegram adapter implements ``delete_message``.""" def test_delete_message_method_exists(self): - telegram = pytest.importorskip("gateway.platforms.telegram") + telegram = pytest.importorskip("plugins.platforms.telegram.adapter") import inspect cls = telegram.TelegramAdapter assert hasattr(cls, "delete_message"), ( diff --git a/tests/gateway/test_stream_consumer_thread_routing.py b/tests/gateway/test_stream_consumer_thread_routing.py index ec4611abfa3b..c86857424576 100644 --- a/tests/gateway/test_stream_consumer_thread_routing.py +++ b/tests/gateway/test_stream_consumer_thread_routing.py @@ -143,7 +143,7 @@ class TestFeishuFallbackThreadRouting: async def test_create_uses_thread_id_when_available(self): """When reply_to=None and metadata has thread_id, message.create should use receive_id_type='thread_id'.""" - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter # We test the _send_raw_message method directly by mocking the client adapter = MagicMock(spec=FeishuAdapter) @@ -200,7 +200,7 @@ class TestFeishuFallbackThreadRouting: async def test_create_uses_chat_id_when_no_thread(self): """When reply_to=None and metadata has no thread_id, message.create should use receive_id_type='chat_id' (original behavior).""" - from gateway.platforms.feishu import FeishuAdapter + from plugins.platforms.feishu.adapter import FeishuAdapter mock_client = MagicMock() mock_create_response = SimpleNamespace( diff --git a/tests/gateway/test_telegram_approval_buttons.py b/tests/gateway/test_telegram_approval_buttons.py index 5810b87a59b0..96de984a9c2d 100644 --- a/tests/gateway/test_telegram_approval_buttons.py +++ b/tests/gateway/test_telegram_approval_buttons.py @@ -46,7 +46,7 @@ def _ensure_telegram_mock(): _ensure_telegram_mock() -from gateway.platforms.telegram import TelegramAdapter +from plugins.platforms.telegram.adapter import TelegramAdapter from gateway.config import Platform, PlatformConfig diff --git a/tests/gateway/test_telegram_callback_auth_fail_closed.py b/tests/gateway/test_telegram_callback_auth_fail_closed.py index 8f6b0fa5afee..ad00c17c0038 100644 --- a/tests/gateway/test_telegram_callback_auth_fail_closed.py +++ b/tests/gateway/test_telegram_callback_auth_fail_closed.py @@ -55,7 +55,7 @@ def _inject_fake_telegram(monkeypatch): def _make_adapter(): - from gateway.platforms.telegram import TelegramAdapter + from plugins.platforms.telegram.adapter import TelegramAdapter config = PlatformConfig(enabled=True, token="fake-token") adapter = object.__new__(TelegramAdapter) diff --git a/tests/gateway/test_telegram_caption_merge.py b/tests/gateway/test_telegram_caption_merge.py index f5d4390f4837..3bb18a225df4 100644 --- a/tests/gateway/test_telegram_caption_merge.py +++ b/tests/gateway/test_telegram_caption_merge.py @@ -1,7 +1,7 @@ """Tests for TelegramPlatform._merge_caption caption deduplication logic.""" -from gateway.platforms.telegram import TelegramAdapter +from plugins.platforms.telegram.adapter import TelegramAdapter merge = TelegramAdapter._merge_caption diff --git a/tests/gateway/test_telegram_channel_posts.py b/tests/gateway/test_telegram_channel_posts.py index ade82c2e4aae..729d5c1ee301 100644 --- a/tests/gateway/test_telegram_channel_posts.py +++ b/tests/gateway/test_telegram_channel_posts.py @@ -63,7 +63,7 @@ def _build_telegram_stubs(): @pytest.fixture def telegram_adapter_cls(monkeypatch): """Import TelegramAdapter without leaking temporary telegram stubs.""" - module_name = "gateway.platforms.telegram" + module_name = "plugins.platforms.telegram.adapter" existing_module = sys.modules.get(module_name) if existing_module is not None: yield existing_module.TelegramAdapter diff --git a/tests/gateway/test_telegram_clarify_buttons.py b/tests/gateway/test_telegram_clarify_buttons.py index 729ee22359a0..81cb5c97ac59 100644 --- a/tests/gateway/test_telegram_clarify_buttons.py +++ b/tests/gateway/test_telegram_clarify_buttons.py @@ -47,7 +47,7 @@ def _ensure_telegram_mock(): _ensure_telegram_mock() -from gateway.platforms.telegram import TelegramAdapter +from plugins.platforms.telegram.adapter import TelegramAdapter from gateway.config import PlatformConfig diff --git a/tests/gateway/test_telegram_conflict.py b/tests/gateway/test_telegram_conflict.py index db132fe05a55..9b33c4bbe604 100644 --- a/tests/gateway/test_telegram_conflict.py +++ b/tests/gateway/test_telegram_conflict.py @@ -34,7 +34,7 @@ def _ensure_telegram_mock(): _ensure_telegram_mock() -from gateway.platforms.telegram import TelegramAdapter # noqa: E402 +from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402 @pytest.fixture(autouse=True) @@ -42,9 +42,9 @@ def _no_auto_discovery(monkeypatch): """Disable DoH auto-discovery so connect() uses the plain builder chain.""" async def _noop(): return [] - monkeypatch.setattr("gateway.platforms.telegram.discover_fallback_ips", _noop) + monkeypatch.setattr("plugins.platforms.telegram.adapter.discover_fallback_ips", _noop) # Mock HTTPXRequest so the builder chain doesn't fail - monkeypatch.setattr("gateway.platforms.telegram.HTTPXRequest", lambda **kwargs: MagicMock()) + monkeypatch.setattr("plugins.platforms.telegram.adapter.HTTPXRequest", lambda **kwargs: MagicMock()) @pytest.mark.asyncio @@ -103,7 +103,7 @@ async def test_polling_conflict_retries_before_fatal(monkeypatch): builder.request.return_value = builder builder.get_updates_request.return_value = builder builder.build.return_value = app - monkeypatch.setattr("gateway.platforms.telegram.Application", SimpleNamespace(builder=MagicMock(return_value=builder))) + monkeypatch.setattr("plugins.platforms.telegram.adapter.Application", SimpleNamespace(builder=MagicMock(return_value=builder))) # Speed up retries for testing monkeypatch.setattr("asyncio.sleep", AsyncMock()) @@ -179,7 +179,7 @@ async def test_polling_conflict_becomes_fatal_after_retries(monkeypatch): builder.request.return_value = builder builder.get_updates_request.return_value = builder builder.build.return_value = app - monkeypatch.setattr("gateway.platforms.telegram.Application", SimpleNamespace(builder=MagicMock(return_value=builder))) + monkeypatch.setattr("plugins.platforms.telegram.adapter.Application", SimpleNamespace(builder=MagicMock(return_value=builder))) # Speed up retries for testing monkeypatch.setattr("asyncio.sleep", AsyncMock()) @@ -232,7 +232,7 @@ async def test_connect_marks_retryable_fatal_error_for_startup_network_failure(m start=AsyncMock(), ) builder.build.return_value = app - monkeypatch.setattr("gateway.platforms.telegram.Application", SimpleNamespace(builder=MagicMock(return_value=builder))) + monkeypatch.setattr("plugins.platforms.telegram.adapter.Application", SimpleNamespace(builder=MagicMock(return_value=builder))) ok = await adapter.connect() @@ -277,7 +277,7 @@ async def test_connect_clears_webhook_before_polling(monkeypatch): builder.get_updates_request.return_value = builder builder.build.return_value = app monkeypatch.setattr( - "gateway.platforms.telegram.Application", + "plugins.platforms.telegram.adapter.Application", SimpleNamespace(builder=MagicMock(return_value=builder)), ) @@ -301,7 +301,7 @@ async def test_disconnect_skips_inactive_updater_and_app(monkeypatch): adapter._app = app warning = MagicMock() - monkeypatch.setattr("gateway.platforms.telegram.logger.warning", warning) + monkeypatch.setattr("plugins.platforms.telegram.adapter.logger.warning", warning) await adapter.disconnect() diff --git a/tests/gateway/test_telegram_documents.py b/tests/gateway/test_telegram_documents.py index f4155107aa0f..b30f809fe393 100644 --- a/tests/gateway/test_telegram_documents.py +++ b/tests/gateway/test_telegram_documents.py @@ -51,7 +51,7 @@ def _ensure_telegram_mock(): _ensure_telegram_mock() # Now we can safely import -from gateway.platforms.telegram import TelegramAdapter # noqa: E402 +from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402 # --------------------------------------------------------------------------- @@ -442,7 +442,7 @@ class TestMediaGroups: msg1 = _make_message(caption="two images", photo=[first_photo]) msg2 = _make_message(photo=[second_photo]) - with patch("gateway.platforms.telegram.cache_image_from_bytes", side_effect=["/tmp/burst-one.jpg", "/tmp/burst-two.jpg"]): + with patch("plugins.platforms.telegram.adapter.cache_image_from_bytes", side_effect=["/tmp/burst-one.jpg", "/tmp/burst-two.jpg"]): await adapter._handle_media_message(_make_update(msg1), MagicMock()) await adapter._handle_media_message(_make_update(msg2), MagicMock()) assert adapter.handle_message.await_count == 0 @@ -462,7 +462,7 @@ class TestMediaGroups: msg1 = _make_message(caption="two images", media_group_id="album-1", photo=[first_photo]) msg2 = _make_message(media_group_id="album-1", photo=[second_photo]) - with patch("gateway.platforms.telegram.cache_image_from_bytes", side_effect=["/tmp/one.jpg", "/tmp/two.jpg"]): + with patch("plugins.platforms.telegram.adapter.cache_image_from_bytes", side_effect=["/tmp/one.jpg", "/tmp/two.jpg"]): await adapter._handle_media_message(_make_update(msg1), MagicMock()) await adapter._handle_media_message(_make_update(msg2), MagicMock()) assert adapter.handle_message.await_count == 0 @@ -479,7 +479,7 @@ class TestMediaGroups: first_photo = _make_photo(_make_file_obj(b"first")) msg = _make_message(caption="two images", media_group_id="album-2", photo=[first_photo]) - with patch("gateway.platforms.telegram.cache_image_from_bytes", return_value="/tmp/one.jpg"): + with patch("plugins.platforms.telegram.adapter.cache_image_from_bytes", return_value="/tmp/one.jpg"): await adapter._handle_media_message(_make_update(msg), MagicMock()) assert "album-2" in adapter._media_group_events @@ -782,8 +782,8 @@ class TestTelegramPhotoBatching: ) with ( - patch("gateway.platforms.telegram.asyncio.current_task", return_value=old_task), - patch("gateway.platforms.telegram.asyncio.sleep", new=AsyncMock()), + patch("plugins.platforms.telegram.adapter.asyncio.current_task", return_value=old_task), + patch("plugins.platforms.telegram.adapter.asyncio.sleep", new=AsyncMock()), ): await adapter._flush_photo_batch(batch_key) diff --git a/tests/gateway/test_telegram_format.py b/tests/gateway/test_telegram_format.py index c8fb121a1735..b29cec031afe 100644 --- a/tests/gateway/test_telegram_format.py +++ b/tests/gateway/test_telegram_format.py @@ -35,7 +35,7 @@ def _ensure_telegram_mock(): _ensure_telegram_mock() -from gateway.platforms.telegram import ( # noqa: E402 +from plugins.platforms.telegram.adapter import ( # noqa: E402 TelegramAdapter, _escape_mdv2, _strip_mdv2, diff --git a/tests/gateway/test_telegram_forum_commands.py b/tests/gateway/test_telegram_forum_commands.py index 0e2ce6d286a1..a68a80526102 100644 --- a/tests/gateway/test_telegram_forum_commands.py +++ b/tests/gateway/test_telegram_forum_commands.py @@ -11,7 +11,7 @@ from gateway.config import Platform, PlatformConfig def _make_test_adapter(): """Build a TelegramAdapter without running __init__.""" - from gateway.platforms.telegram import TelegramAdapter + from plugins.platforms.telegram.adapter import TelegramAdapter adapter = object.__new__(TelegramAdapter) adapter.platform = Platform.TELEGRAM diff --git a/tests/gateway/test_telegram_group_gating.py b/tests/gateway/test_telegram_group_gating.py index f5fb112f136e..e1d238562f4c 100644 --- a/tests/gateway/test_telegram_group_gating.py +++ b/tests/gateway/test_telegram_group_gating.py @@ -23,7 +23,7 @@ def _make_adapter( observe_unmentioned_group_messages=None, bot_username="hermes_bot", ): - from gateway.platforms.telegram import TelegramAdapter + from plugins.platforms.telegram.adapter import TelegramAdapter extra = {} if require_mention is not None: diff --git a/tests/gateway/test_telegram_max_doc_bytes.py b/tests/gateway/test_telegram_max_doc_bytes.py index 163dcc9f5764..95f3c3029b9a 100644 --- a/tests/gateway/test_telegram_max_doc_bytes.py +++ b/tests/gateway/test_telegram_max_doc_bytes.py @@ -29,7 +29,7 @@ def _ensure_telegram_mock(): _ensure_telegram_mock() -from gateway.platforms.telegram import TelegramAdapter # noqa: E402 +from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402 def test_max_doc_bytes_defaults_to_20mb_without_base_url(): diff --git a/tests/gateway/test_telegram_mention_boundaries.py b/tests/gateway/test_telegram_mention_boundaries.py index 2a203857efbb..cc99d15f5bd1 100644 --- a/tests/gateway/test_telegram_mention_boundaries.py +++ b/tests/gateway/test_telegram_mention_boundaries.py @@ -14,7 +14,7 @@ those contexts. from types import SimpleNamespace from gateway.config import Platform, PlatformConfig -from gateway.platforms.telegram import TelegramAdapter +from plugins.platforms.telegram.adapter import TelegramAdapter def _make_adapter(): diff --git a/tests/gateway/test_telegram_model_picker.py b/tests/gateway/test_telegram_model_picker.py index f6c887ef3f47..10052e0de332 100644 --- a/tests/gateway/test_telegram_model_picker.py +++ b/tests/gateway/test_telegram_model_picker.py @@ -32,7 +32,7 @@ def _ensure_telegram_mock(): _ensure_telegram_mock() from gateway.config import PlatformConfig -from gateway.platforms.telegram import TelegramAdapter +from plugins.platforms.telegram.adapter import TelegramAdapter def _make_adapter(): @@ -157,7 +157,7 @@ class TestTelegramModelPicker: which is robust to whether `telegram` is the real SDK or the module mock (the SDK markup objects don't expose a plain iterable under the mock).""" - import gateway.platforms.telegram as tg + import plugins.platforms.telegram.adapter as tg built: list = [] diff --git a/tests/gateway/test_telegram_network.py b/tests/gateway/test_telegram_network.py index fe50fb8c57e4..57950d0fb61f 100644 --- a/tests/gateway/test_telegram_network.py +++ b/tests/gateway/test_telegram_network.py @@ -1,4 +1,4 @@ -"""Tests for gateway.platforms.telegram_network – fallback transport layer. +"""Tests for plugins.platforms.telegram.telegram_network – fallback transport layer. Background ---------- @@ -18,7 +18,7 @@ fallback IPs in order, then "stick" to whichever IP works. import httpx import pytest -from gateway.platforms import telegram_network as tnet +import plugins.platforms.telegram.telegram_network as tnet # --------------------------------------------------------------------------- @@ -438,7 +438,7 @@ class TestAdapterFallbackIps: sys.modules.setdefault(name, mod) from gateway.config import PlatformConfig - from gateway.platforms.telegram import TelegramAdapter + from plugins.platforms.telegram.adapter import TelegramAdapter config = PlatformConfig(enabled=True, token="test-token") if extra: diff --git a/tests/gateway/test_telegram_network_reconnect.py b/tests/gateway/test_telegram_network_reconnect.py index 81b7bed12e43..bd9e9e3b7b00 100644 --- a/tests/gateway/test_telegram_network_reconnect.py +++ b/tests/gateway/test_telegram_network_reconnect.py @@ -33,7 +33,7 @@ def _ensure_telegram_mock(): _ensure_telegram_mock() -from gateway.platforms.telegram import TelegramAdapter # noqa: E402 +from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402 @pytest.fixture(autouse=True) @@ -41,7 +41,7 @@ def _no_auto_discovery(monkeypatch): """Disable DoH auto-discovery so connect() uses the plain builder chain.""" async def _noop(): return [] - monkeypatch.setattr("gateway.platforms.telegram.discover_fallback_ips", _noop) + monkeypatch.setattr("plugins.platforms.telegram.adapter.discover_fallback_ips", _noop) def _make_adapter() -> TelegramAdapter: @@ -379,7 +379,7 @@ async def test_heartbeat_probe_reenters_ladder_when_get_me_times_out(): raise asyncio.TimeoutError() with patch("asyncio.sleep", new_callable=AsyncMock): - with patch("gateway.platforms.telegram.asyncio.wait_for", new=fast_wait_for): + with patch("plugins.platforms.telegram.adapter.asyncio.wait_for", new=fast_wait_for): await adapter._verify_polling_after_reconnect() adapter._handle_polling_network_error.assert_awaited_once() diff --git a/tests/gateway/test_telegram_reactions.py b/tests/gateway/test_telegram_reactions.py index 8b3b0686bb41..70c2fd4ee846 100644 --- a/tests/gateway/test_telegram_reactions.py +++ b/tests/gateway/test_telegram_reactions.py @@ -11,7 +11,7 @@ from gateway.session import SessionSource def _make_adapter(**extra_env): - from gateway.platforms.telegram import TelegramAdapter + from plugins.platforms.telegram.adapter import TelegramAdapter adapter = object.__new__(TelegramAdapter) adapter.platform = Platform.TELEGRAM diff --git a/tests/gateway/test_telegram_reply_mode.py b/tests/gateway/test_telegram_reply_mode.py index f036dc6b785f..66b471aadbed 100644 --- a/tests/gateway/test_telegram_reply_mode.py +++ b/tests/gateway/test_telegram_reply_mode.py @@ -31,7 +31,7 @@ def _ensure_telegram_mock(): _ensure_telegram_mock() -from gateway.platforms.telegram import TelegramAdapter # noqa: E402 +from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402 @pytest.fixture() diff --git a/tests/gateway/test_telegram_reply_quote.py b/tests/gateway/test_telegram_reply_quote.py index d636f0df94af..f9c8d27aa262 100644 --- a/tests/gateway/test_telegram_reply_quote.py +++ b/tests/gateway/test_telegram_reply_quote.py @@ -33,7 +33,7 @@ def _ensure_telegram_mock(): _ensure_telegram_mock() -from gateway.platforms.telegram import TelegramAdapter # noqa: E402 +from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402 def _make_adapter(): diff --git a/tests/gateway/test_telegram_send_draft_format.py b/tests/gateway/test_telegram_send_draft_format.py index a84a42852e0c..6608a365d532 100644 --- a/tests/gateway/test_telegram_send_draft_format.py +++ b/tests/gateway/test_telegram_send_draft_format.py @@ -35,8 +35,8 @@ def _ensure_telegram_mock(): _ensure_telegram_mock() -from gateway.platforms import telegram as tg_mod # noqa: E402 -from gateway.platforms.telegram import TelegramAdapter # noqa: E402 +import plugins.platforms.telegram.adapter as tg_mod # noqa: E402 +from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402 def _make_adapter() -> TelegramAdapter: diff --git a/tests/gateway/test_telegram_send_path_health.py b/tests/gateway/test_telegram_send_path_health.py index 05972bdba437..d5285f251099 100644 --- a/tests/gateway/test_telegram_send_path_health.py +++ b/tests/gateway/test_telegram_send_path_health.py @@ -27,7 +27,7 @@ def _ensure_telegram_mock(): _ensure_telegram_mock() -from gateway.platforms.telegram import TelegramAdapter # noqa: E402 +from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402 def _make_adapter() -> TelegramAdapter: @@ -78,12 +78,12 @@ async def test_reconnect_storm_sets_and_heartbeat_clears_flag(monkeypatch): adapter._app.bot.get_me = AsyncMock(return_value=MagicMock()) adapter._polling_error_callback_ref = AsyncMock() monkeypatch.setattr( - "gateway.platforms.telegram.Update", MagicMock(ALL_TYPES=[]) + "plugins.platforms.telegram.adapter.Update", MagicMock(ALL_TYPES=[]) ) await adapter._handle_polling_network_error(OSError("Bad Gateway")) assert adapter._send_path_degraded is True - with patch("gateway.platforms.telegram.asyncio.sleep", new_callable=AsyncMock): + with patch("plugins.platforms.telegram.adapter.asyncio.sleep", new_callable=AsyncMock): await adapter._verify_polling_after_reconnect() assert adapter._send_path_degraded is False diff --git a/tests/gateway/test_telegram_slash_confirm.py b/tests/gateway/test_telegram_slash_confirm.py index 785d9f7c6ace..ef321d817ab1 100644 --- a/tests/gateway/test_telegram_slash_confirm.py +++ b/tests/gateway/test_telegram_slash_confirm.py @@ -34,7 +34,7 @@ def _ensure_telegram_mock(): _ensure_telegram_mock() -from gateway.platforms.telegram import TelegramAdapter +from plugins.platforms.telegram.adapter import TelegramAdapter from gateway.config import PlatformConfig diff --git a/tests/gateway/test_telegram_status_update.py b/tests/gateway/test_telegram_status_update.py index f49ca9c60e17..85dc1f04053f 100644 --- a/tests/gateway/test_telegram_status_update.py +++ b/tests/gateway/test_telegram_status_update.py @@ -64,7 +64,7 @@ def _install_fake_telegram(monkeypatch): @pytest.fixture def adapter(monkeypatch): _install_fake_telegram(monkeypatch) - from gateway.platforms.telegram import TelegramAdapter + from plugins.platforms.telegram.adapter import TelegramAdapter a = TelegramAdapter(PlatformConfig(enabled=True, token="fake-token")) a._bot = MagicMock() diff --git a/tests/gateway/test_telegram_text_batch_perf.py b/tests/gateway/test_telegram_text_batch_perf.py index 194dd0d3ffbc..e17365a7771e 100644 --- a/tests/gateway/test_telegram_text_batch_perf.py +++ b/tests/gateway/test_telegram_text_batch_perf.py @@ -16,7 +16,7 @@ import math import pytest -from gateway.platforms.telegram import TelegramAdapter +from plugins.platforms.telegram.adapter import TelegramAdapter @pytest.fixture diff --git a/tests/gateway/test_telegram_text_batching.py b/tests/gateway/test_telegram_text_batching.py index 5cd451900670..d506e6a50bd4 100644 --- a/tests/gateway/test_telegram_text_batching.py +++ b/tests/gateway/test_telegram_text_batching.py @@ -18,7 +18,7 @@ from gateway.session import build_session_key def _make_adapter(): """Create a minimal TelegramAdapter for testing text batching.""" - from gateway.platforms.telegram import TelegramAdapter + from plugins.platforms.telegram.adapter import TelegramAdapter config = PlatformConfig(enabled=True, token="test-token") adapter = object.__new__(TelegramAdapter) diff --git a/tests/gateway/test_telegram_thread_fallback.py b/tests/gateway/test_telegram_thread_fallback.py index 036d27e771bf..20b38a7cbe41 100644 --- a/tests/gateway/test_telegram_thread_fallback.py +++ b/tests/gateway/test_telegram_thread_fallback.py @@ -116,7 +116,7 @@ def _inject_fake_telegram(monkeypatch): def _make_adapter(): - from gateway.platforms.telegram import TelegramAdapter + from plugins.platforms.telegram.adapter import TelegramAdapter config = PlatformConfig(enabled=True, token="fake-token") adapter = object.__new__(TelegramAdapter) @@ -137,7 +137,7 @@ def _make_adapter(): def test_non_forum_group_reply_thread_id_does_not_fork_session_key(): """Reply-derived thread ids in ordinary groups must not create topic lanes.""" - from gateway.platforms import telegram as telegram_mod + import plugins.platforms.telegram.adapter as telegram_mod adapter = _make_adapter() message = SimpleNamespace( @@ -171,7 +171,7 @@ def test_non_forum_group_reply_thread_id_does_not_fork_session_key(): def test_forum_group_topic_message_preserves_thread_session_key(): """Real Telegram forum-topic messages should still route by topic id.""" - from gateway.platforms import telegram as telegram_mod + import plugins.platforms.telegram.adapter as telegram_mod adapter = _make_adapter() message = SimpleNamespace( @@ -201,7 +201,7 @@ def test_forum_group_topic_message_preserves_thread_session_key(): def test_forum_general_topic_without_message_thread_id_keeps_thread_context(): """Forum General-topic messages should keep synthetic thread context.""" - from gateway.platforms import telegram as telegram_mod + import plugins.platforms.telegram.adapter as telegram_mod adapter = _make_adapter() message = SimpleNamespace( diff --git a/tests/gateway/test_text_batching.py b/tests/gateway/test_text_batching.py index c0e7bf5d4b61..f2876fbbc959 100644 --- a/tests/gateway/test_text_batching.py +++ b/tests/gateway/test_text_batching.py @@ -218,7 +218,7 @@ class TestDiscordTextBatching: def _make_matrix_adapter(): """Create a minimal MatrixAdapter for testing text batching.""" - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter config = PlatformConfig(enabled=True, token="test-token") adapter = object.__new__(MatrixAdapter) @@ -388,7 +388,7 @@ class TestWeComTextBatching: def _make_telegram_adapter(): """Create a minimal TelegramAdapter for testing adaptive delay.""" - from gateway.platforms.telegram import TelegramAdapter + from plugins.platforms.telegram.adapter import TelegramAdapter config = PlatformConfig(enabled=True, token="test-token") adapter = object.__new__(TelegramAdapter) @@ -452,7 +452,7 @@ class TestTelegramAdaptiveDelay: def _make_feishu_adapter(): """Create a minimal FeishuAdapter for testing adaptive delay.""" - from gateway.platforms.feishu import FeishuAdapter, FeishuBatchState + from plugins.platforms.feishu.adapter import FeishuAdapter, FeishuBatchState config = PlatformConfig(enabled=True, token="test-token") adapter = object.__new__(FeishuAdapter) diff --git a/tests/gateway/test_whatsapp_connect.py b/tests/gateway/test_whatsapp_connect.py index 9d7807734bbc..2ae5f2b06d2a 100644 --- a/tests/gateway/test_whatsapp_connect.py +++ b/tests/gateway/test_whatsapp_connect.py @@ -40,7 +40,7 @@ class _AsyncCM: def _make_adapter(): """Create a WhatsAppAdapter with test attributes (bypass __init__).""" - from gateway.platforms.whatsapp import WhatsAppAdapter + from plugins.platforms.whatsapp.adapter import WhatsAppAdapter adapter = WhatsAppAdapter.__new__(WhatsAppAdapter) adapter.platform = Platform.WHATSAPP @@ -85,18 +85,18 @@ def _mock_aiohttp(status=200, json_data=None, json_side_effect=None): def _connect_patches(mock_proc, mock_fh, mock_client_cls=None): """Return a dict of common patches needed to reach the health-check loop.""" patches = { - "gateway.platforms.whatsapp.check_whatsapp_requirements": True, - "gateway.platforms.whatsapp.asyncio.create_task": MagicMock(), + "plugins.platforms.whatsapp.adapter.check_whatsapp_requirements": True, + "plugins.platforms.whatsapp.adapter.asyncio.create_task": MagicMock(), } base = [ - patch("gateway.platforms.whatsapp.check_whatsapp_requirements", return_value=True), + patch("plugins.platforms.whatsapp.adapter.check_whatsapp_requirements", return_value=True), patch.object(Path, "exists", return_value=True), patch.object(Path, "mkdir", return_value=None), patch("subprocess.run", return_value=MagicMock(returncode=0)), patch("subprocess.Popen", return_value=mock_proc), patch("builtins.open", return_value=mock_fh), - patch("gateway.platforms.whatsapp.asyncio.sleep", new_callable=AsyncMock), - patch("gateway.platforms.whatsapp.asyncio.create_task"), + patch("plugins.platforms.whatsapp.adapter.asyncio.sleep", new_callable=AsyncMock), + patch("plugins.platforms.whatsapp.adapter.asyncio.create_task"), ] if mock_client_cls is not None: base.append(patch("aiohttp.ClientSession", mock_client_cls)) @@ -112,7 +112,7 @@ class TestCloseBridgeLog: @staticmethod def _bare_adapter(): - from gateway.platforms.whatsapp import WhatsAppAdapter + from plugins.platforms.whatsapp.adapter import WhatsAppAdapter a = WhatsAppAdapter.__new__(WhatsAppAdapter) a._bridge_log_fh = None return a @@ -223,7 +223,7 @@ class TestConnectCleanup: install_result = MagicMock(returncode=1, stderr="install failed") - with patch("gateway.platforms.whatsapp.check_whatsapp_requirements", return_value=True), \ + with patch("plugins.platforms.whatsapp.adapter.check_whatsapp_requirements", return_value=True), \ patch.object(Path, "exists", autospec=True, side_effect=_path_exists), \ patch("subprocess.run", return_value=install_result), \ patch("gateway.status.acquire_scoped_lock", return_value=(True, None)), \ @@ -402,7 +402,7 @@ class TestBridgeRuntimeFailure: mock_fh = MagicMock() - with patch("gateway.platforms.whatsapp.check_whatsapp_requirements", return_value=True), \ + with patch("plugins.platforms.whatsapp.adapter.check_whatsapp_requirements", return_value=True), \ patch.object(Path, "exists", return_value=True), \ patch.object(Path, "mkdir", return_value=None), \ patch("subprocess.run", return_value=MagicMock(returncode=0)), \ @@ -423,7 +423,7 @@ class TestKillPortProcess: """Verify _kill_port_process uses platform-appropriate commands.""" def test_uses_netstat_and_taskkill_on_windows(self): - from gateway.platforms.whatsapp import _kill_port_process + from plugins.platforms.whatsapp.adapter import _kill_port_process netstat_output = ( " Proto Local Address Foreign Address State PID\n" @@ -440,8 +440,8 @@ class TestKillPortProcess: return mock_taskkill return MagicMock() - with patch("gateway.platforms.whatsapp._IS_WINDOWS", True), \ - patch("gateway.platforms.whatsapp.subprocess.run", side_effect=run_side_effect) as mock_run: + with patch("plugins.platforms.whatsapp.adapter._IS_WINDOWS", True), \ + patch("plugins.platforms.whatsapp.adapter.subprocess.run", side_effect=run_side_effect) as mock_run: _kill_port_process(3000) # netstat called @@ -455,15 +455,15 @@ class TestKillPortProcess: ) def test_does_not_kill_wrong_port_on_windows(self): - from gateway.platforms.whatsapp import _kill_port_process + from plugins.platforms.whatsapp.adapter import _kill_port_process netstat_output = ( " TCP 0.0.0.0:30000 0.0.0.0:0 LISTENING 55555\n" ) mock_netstat = MagicMock(stdout=netstat_output) - with patch("gateway.platforms.whatsapp._IS_WINDOWS", True), \ - patch("gateway.platforms.whatsapp.subprocess.run", return_value=mock_netstat) as mock_run: + with patch("plugins.platforms.whatsapp.adapter._IS_WINDOWS", True), \ + patch("plugins.platforms.whatsapp.adapter.subprocess.run", return_value=mock_netstat) as mock_run: _kill_port_process(3000) # Should NOT call taskkill because port 30000 != 3000 @@ -473,12 +473,12 @@ class TestKillPortProcess: ) def test_uses_fuser_on_linux(self): - from gateway.platforms.whatsapp import _kill_port_process + from plugins.platforms.whatsapp.adapter import _kill_port_process mock_check = MagicMock(returncode=0) - with patch("gateway.platforms.whatsapp._IS_WINDOWS", False), \ - patch("gateway.platforms.whatsapp.subprocess.run", return_value=mock_check) as mock_run: + with patch("plugins.platforms.whatsapp.adapter._IS_WINDOWS", False), \ + patch("plugins.platforms.whatsapp.adapter.subprocess.run", return_value=mock_check) as mock_run: _kill_port_process(3000) calls = [c.args[0] for c in mock_run.call_args_list] @@ -486,12 +486,12 @@ class TestKillPortProcess: assert ["fuser", "-k", "3000/tcp"] in calls def test_skips_fuser_kill_when_port_free(self): - from gateway.platforms.whatsapp import _kill_port_process + from plugins.platforms.whatsapp.adapter import _kill_port_process mock_check = MagicMock(returncode=1) # port not in use - with patch("gateway.platforms.whatsapp._IS_WINDOWS", False), \ - patch("gateway.platforms.whatsapp.subprocess.run", return_value=mock_check) as mock_run: + with patch("plugins.platforms.whatsapp.adapter._IS_WINDOWS", False), \ + patch("plugins.platforms.whatsapp.adapter.subprocess.run", return_value=mock_check) as mock_run: _kill_port_process(3000) calls = [c.args[0] for c in mock_run.call_args_list] @@ -499,10 +499,10 @@ class TestKillPortProcess: assert ["fuser", "-k", "3000/tcp"] not in calls def test_suppresses_exceptions(self): - from gateway.platforms.whatsapp import _kill_port_process + from plugins.platforms.whatsapp.adapter import _kill_port_process - with patch("gateway.platforms.whatsapp._IS_WINDOWS", True), \ - patch("gateway.platforms.whatsapp.subprocess.run", side_effect=OSError("no netstat")): + with patch("plugins.platforms.whatsapp.adapter._IS_WINDOWS", True), \ + patch("plugins.platforms.whatsapp.adapter.subprocess.run", side_effect=OSError("no netstat")): _kill_port_process(3000) # must not raise @@ -526,9 +526,9 @@ class TestHttpSessionLifecycle: adapter._running = True adapter._session_lock_identity = None - with patch("gateway.platforms.whatsapp._IS_WINDOWS", True), \ - patch("gateway.platforms.whatsapp.subprocess.run", return_value=MagicMock(returncode=0)) as mock_run, \ - patch("gateway.platforms.whatsapp.asyncio.sleep", new_callable=AsyncMock): + with patch("plugins.platforms.whatsapp.adapter._IS_WINDOWS", True), \ + patch("plugins.platforms.whatsapp.adapter.subprocess.run", return_value=MagicMock(returncode=0)) as mock_run, \ + patch("plugins.platforms.whatsapp.adapter.asyncio.sleep", new_callable=AsyncMock): await adapter.disconnect() mock_run.assert_called_once_with( @@ -634,7 +634,7 @@ class TestNoCredsPreflight: @pytest.mark.asyncio async def test_connect_returns_false_when_no_creds(self, tmp_path): - from gateway.platforms.whatsapp import WhatsAppAdapter + from plugins.platforms.whatsapp.adapter import WhatsAppAdapter adapter = WhatsAppAdapter.__new__(WhatsAppAdapter) adapter.platform = Platform.WHATSAPP @@ -654,7 +654,7 @@ class TestNoCredsPreflight: adapter._fatal_error_retryable = True with patch( - "gateway.platforms.whatsapp.check_whatsapp_requirements", + "plugins.platforms.whatsapp.adapter.check_whatsapp_requirements", return_value=True, ): result = await adapter.connect() @@ -670,7 +670,7 @@ class TestNoCredsPreflight: connect() proceeds to the bridge bootstrap path. We don't fully simulate the bridge here — we just verify no fast-fail occurs. """ - from gateway.platforms.whatsapp import WhatsAppAdapter + from plugins.platforms.whatsapp.adapter import WhatsAppAdapter adapter = WhatsAppAdapter.__new__(WhatsAppAdapter) adapter.platform = Platform.WHATSAPP @@ -692,7 +692,7 @@ class TestNoCredsPreflight: adapter._acquire_platform_lock = MagicMock(return_value=False) with patch( - "gateway.platforms.whatsapp.check_whatsapp_requirements", + "plugins.platforms.whatsapp.adapter.check_whatsapp_requirements", return_value=True, ): result = await adapter.connect() diff --git a/tests/gateway/test_whatsapp_formatting.py b/tests/gateway/test_whatsapp_formatting.py index 04b3174cdc29..f814199c0511 100644 --- a/tests/gateway/test_whatsapp_formatting.py +++ b/tests/gateway/test_whatsapp_formatting.py @@ -20,7 +20,7 @@ from gateway.config import Platform def _make_adapter(): """Create a WhatsAppAdapter with test attributes (bypass __init__).""" - from gateway.platforms.whatsapp import WhatsAppAdapter + from plugins.platforms.whatsapp.adapter import WhatsAppAdapter adapter = WhatsAppAdapter.__new__(WhatsAppAdapter) adapter.platform = Platform.WHATSAPP @@ -146,7 +146,7 @@ class TestMessageLimits: """WhatsApp message length limits.""" def test_max_message_length_is_practical(self): - from gateway.platforms.whatsapp import WhatsAppAdapter + from plugins.platforms.whatsapp.adapter import WhatsAppAdapter assert WhatsAppAdapter.MAX_MESSAGE_LENGTH == 4096 def test_chunk_limit_reserves_default_self_chat_prefix(self, monkeypatch): diff --git a/tests/gateway/test_whatsapp_group_gating.py b/tests/gateway/test_whatsapp_group_gating.py index 755606338396..cee3894d6e01 100644 --- a/tests/gateway/test_whatsapp_group_gating.py +++ b/tests/gateway/test_whatsapp_group_gating.py @@ -6,7 +6,7 @@ from gateway.config import Platform, PlatformConfig, load_gateway_config def _make_adapter(require_mention=None, mention_patterns=None, free_response_chats=None, dm_policy=None, allow_from=None, group_policy=None, group_allow_from=None): - from gateway.platforms.whatsapp import WhatsAppAdapter + from plugins.platforms.whatsapp.adapter import WhatsAppAdapter extra = {} if require_mention is not None: @@ -358,7 +358,7 @@ def test_real_dm_still_processed_after_broadcast_filter(): def test_is_broadcast_chat_helper_recognizes_common_jids(): - from gateway.platforms.whatsapp import WhatsAppAdapter + from plugins.platforms.whatsapp.adapter import WhatsAppAdapter assert WhatsAppAdapter._is_broadcast_chat("status@broadcast") is True assert WhatsAppAdapter._is_broadcast_chat("STATUS@BROADCAST") is True diff --git a/tests/gateway/test_whatsapp_reply_prefix.py b/tests/gateway/test_whatsapp_reply_prefix.py index 61f373326659..867022ac7394 100644 --- a/tests/gateway/test_whatsapp_reply_prefix.py +++ b/tests/gateway/test_whatsapp_reply_prefix.py @@ -87,19 +87,19 @@ class TestAdapterInit: """Test that WhatsAppAdapter reads reply_prefix from config.extra.""" def test_reply_prefix_from_extra(self): - from gateway.platforms.whatsapp import WhatsAppAdapter + 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" def test_reply_prefix_default_none(self): - from gateway.platforms.whatsapp import WhatsAppAdapter + from plugins.platforms.whatsapp.adapter import WhatsAppAdapter config = PlatformConfig(enabled=True) adapter = WhatsAppAdapter(config) assert adapter._reply_prefix is None def test_reply_prefix_empty_string(self): - from gateway.platforms.whatsapp import WhatsAppAdapter + from plugins.platforms.whatsapp.adapter import WhatsAppAdapter config = PlatformConfig(enabled=True, extra={"reply_prefix": ""}) adapter = WhatsAppAdapter(config) assert adapter._reply_prefix == "" diff --git a/tests/gateway/test_whatsapp_text_batching.py b/tests/gateway/test_whatsapp_text_batching.py index 4258617c6784..a4d2816c389c 100644 --- a/tests/gateway/test_whatsapp_text_batching.py +++ b/tests/gateway/test_whatsapp_text_batching.py @@ -12,7 +12,7 @@ import asyncio from gateway.config import Platform, PlatformConfig from gateway.platforms.base import MessageEvent, MessageType -from gateway.platforms.whatsapp import WhatsAppAdapter +from plugins.platforms.whatsapp.adapter import WhatsAppAdapter from gateway.session import SessionSource diff --git a/tests/gateway/test_ws_auth_retry.py b/tests/gateway/test_ws_auth_retry.py index ada5799538be..997afed733b2 100644 --- a/tests/gateway/test_ws_auth_retry.py +++ b/tests/gateway/test_ws_auth_retry.py @@ -123,7 +123,7 @@ class TestMatrixSyncAuthRetry: nio_mock.SyncError = SyncError - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter adapter = MatrixAdapter.__new__(MatrixAdapter) adapter._closing = False @@ -154,7 +154,7 @@ class TestMatrixSyncAuthRetry: def test_exception_with_401_stops_loop(self): """An exception containing '401' should stop syncing.""" - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter adapter = MatrixAdapter.__new__(MatrixAdapter) adapter._closing = False @@ -189,7 +189,7 @@ class TestMatrixSyncAuthRetry: def test_transient_error_retries(self): """A transient error should retry (not stop immediately).""" - from gateway.platforms.matrix import MatrixAdapter + from plugins.platforms.matrix.adapter import MatrixAdapter adapter = MatrixAdapter.__new__(MatrixAdapter) adapter._closing = False diff --git a/tests/hermes_cli/test_logs.py b/tests/hermes_cli/test_logs.py index 52fa63e3ec95..286f8c85392f 100644 --- a/tests/hermes_cli/test_logs.py +++ b/tests/hermes_cli/test_logs.py @@ -87,8 +87,8 @@ class TestExtractLoggerName: assert _extract_logger_name(line) == "gateway.run" def test_nested_logger(self): - line = "2026-04-11 10:23:45 INFO gateway.platforms.telegram: connected" - assert _extract_logger_name(line) == "gateway.platforms.telegram" + line = "2026-04-11 10:23:45 INFO plugins.platforms.telegram.adapter: connected" + assert _extract_logger_name(line) == "plugins.platforms.telegram.adapter" def test_warning_level(self): line = "2026-04-11 10:23:45 WARNING tools.terminal_tool: timeout" @@ -116,7 +116,7 @@ class TestLineMatchesComponent: assert _line_matches_component(line, ("gateway",)) def test_gateway_nested(self): - line = "2026-04-11 10:23:45 INFO gateway.platforms.telegram: msg" + line = "2026-04-11 10:23:45 INFO plugins.platforms.telegram.adapter: msg" assert _line_matches_component(line, ("gateway",)) def test_tools_component(self): diff --git a/tests/test_hermes_logging.py b/tests/test_hermes_logging.py index 38672da54f58..807d542c26d3 100644 --- a/tests/test_hermes_logging.py +++ b/tests/test_hermes_logging.py @@ -305,7 +305,7 @@ class TestGatewayMode: """gateway.log captures records from gateway.* loggers.""" hermes_logging.setup_logging(hermes_home=hermes_home, mode="gateway") - gw_logger = logging.getLogger("gateway.platforms.telegram") + gw_logger = logging.getLogger("plugins.platforms.telegram.adapter") gw_logger.info("telegram connected") for h in logging.getLogger().handlers: @@ -554,7 +554,7 @@ class TestComponentFilter: def test_passes_nested_matching_prefix(self): f = hermes_logging._ComponentFilter(("gateway",)) record = logging.LogRecord( - "gateway.platforms.telegram", logging.INFO, "", 0, "msg", (), None + "plugins.platforms.telegram.adapter", logging.INFO, "", 0, "msg", (), None ) assert f.filter(record) is True diff --git a/tests/tools/test_send_message_tool.py b/tests/tools/test_send_message_tool.py index 63f45e1e75c6..002af15bb6e4 100644 --- a/tests/tools/test_send_message_tool.py +++ b/tests/tools/test_send_message_tool.py @@ -817,7 +817,7 @@ class TestSendToPlatformChunking: fake_module = SimpleNamespace(MatrixAdapter=FakeAdapter) - with patch.dict(sys.modules, {"gateway.platforms.matrix": fake_module}): + with patch.dict(sys.modules, {"plugins.platforms.matrix.adapter": fake_module}): result = asyncio.run( _send_matrix_via_adapter( SimpleNamespace(enabled=True, token="tok", extra={"homeserver": "https://matrix.example.com"}),