from __future__ import annotations import asyncio from unittest.mock import AsyncMock, MagicMock import pytest from yuxi.channels.models import ( ChannelIdentity, ChannelMessage, ChannelResponse, ChannelStatus, ChannelType, ChatType, DeliveryResult, MessageType, ) from yuxi.channels.adapters.wechat.adapter import WeChatAdapter class TestWeChatAdapter: @pytest.fixture def wecom_config(self): return { "mode": "wecom", "corp_id": "test_corp_id", "corp_secret": "test_corp_secret", "agent_id": "1000001", "dm_policy": "open", } @pytest.fixture def mp_config(self): return { "mode": "mp", "app_id": "test_app_id", "app_secret": "test_app_secret", "dm_policy": "open", } @pytest.fixture def personal_config(self): return { "mode": "personal", "bridge_url": "http://localhost:5555", "dm_policy": "open", } def test_channel_id(self): adapter = WeChatAdapter({"dm_policy": "open"}) assert adapter.channel_id == "wechat" def test_channel_type(self): adapter = WeChatAdapter({"dm_policy": "open"}) assert adapter.channel_type == ChannelType.WECHAT def test_capabilities(self): adapter = WeChatAdapter({"dm_policy": "open"}) assert adapter.supports_streaming is False assert adapter.supports_markdown is False assert adapter.text_chunk_limit == 2048 assert adapter.max_media_size_mb == 20 def test_mode_selection_wecom(self, wecom_config): adapter = WeChatAdapter(wecom_config) assert adapter._mode == "personal" def test_mode_selection_mp(self, mp_config): adapter = WeChatAdapter(mp_config) assert adapter._mode == "personal" def test_mode_selection_personal(self, personal_config): adapter = WeChatAdapter(personal_config) assert adapter._mode == "personal" class TestNormalizeInbound: @pytest.fixture def wecom_adapter(self): config = {"corp_id": "test", "corp_secret": "test", "agent_id": "1000001", "dm_policy": "open"} adapter = WeChatAdapter(config) adapter._mode = "wecom" return adapter @pytest.fixture def mp_adapter(self): config = {"app_id": "test", "app_secret": "test", "dm_policy": "open"} adapter = WeChatAdapter(config) adapter._mode = "mp" return adapter @pytest.fixture def personal_adapter(self): config = {"bridge_url": "http://localhost:5555", "dm_policy": "open"} adapter = WeChatAdapter(config) adapter._mode = "personal" return adapter def test_normalize_wecom_text_message(self, wecom_adapter): payload = { "FromUserName": "user123", "ToUserName": "agent456", "MsgId": "789012", "MsgType": "text", "Content": "你好", } result = wecom_adapter.normalize_inbound(payload) assert isinstance(result, ChannelMessage) assert result.content == "你好" assert result.message_type == MessageType.TEXT assert result.identity.channel_user_id == "user123" assert result.identity.channel_message_id == "789012" assert result.metadata["wechat_mode"] == "wecom" def test_normalize_wecom_image_message(self, wecom_adapter): payload = { "FromUserName": "user123", "ToUserName": "agent456", "MsgId": "789013", "MsgType": "image", "MediaId": "media_abc", "PicUrl": "http://example.com/pic.jpg", } result = wecom_adapter.normalize_inbound(payload) assert result.message_type == MessageType.IMAGE assert len(result.attachments) == 1 assert result.attachments[0].type == "image" assert result.attachments[0].file_id == "media_abc" def test_normalize_mp_text_message(self, mp_adapter): payload = { "FromUserName": "openid_abc", "ToUserName": "mp_appid_xyz", "MsgId": "123456", "MsgType": "text", "Content": "你好公众号", } result = mp_adapter.normalize_inbound(payload) assert result.content == "你好公众号" assert result.message_type == MessageType.TEXT assert result.identity.channel_user_id == "openid_abc" assert result.identity.channel_chat_id == "openid_abc" assert result.metadata["wechat_mode"] == "mp" def test_normalize_bridge_direct_message(self, personal_adapter): payload = { "sender_id": "wxid_abc", "chat_id": "wxid_abc", "msg_id": "111", "content": "你好", "is_group": False, "msg_type": 1, } result = personal_adapter.normalize_inbound(payload) assert result.content == "你好" assert result.message_type == MessageType.TEXT assert result.chat_type == ChatType.DIRECT assert result.identity.channel_user_id == "wxid_abc" assert result.metadata["wechat_mode"] == "personal" def test_normalize_bridge_group_message(self, personal_adapter): payload = { "sender_id": "wxid_abc", "chat_id": "room_123", "msg_id": "222", "content": "群聊消息", "is_group": True, "msg_type": 1, } result = personal_adapter.normalize_inbound(payload) assert result.content == "群聊消息" assert result.chat_type == ChatType.GROUP assert result.identity.channel_chat_id == "room_123" assert result.metadata["room_id"] == "room_123" assert result.metadata["sender_wxid"] == "wxid_abc" def test_normalize_webhook_xml_body(self, wecom_adapter): xml_body = ( "agent" "user" "text" "test" "123" ) result = wecom_adapter.normalize_inbound(xml_body) assert result.content == "test" assert result.identity.channel_user_id == "user" assert result.metadata["wechat_mode"] == "wecom" class TestFormatOutbound: @pytest.fixture def wecom_adapter(self): config = {"corp_id": "test", "corp_secret": "test", "agent_id": "1000001", "dm_policy": "open"} adapter = WeChatAdapter(config) adapter._mode = "wecom" return adapter @pytest.fixture def mp_adapter(self): config = {"app_id": "test", "app_secret": "test", "dm_policy": "open"} adapter = WeChatAdapter(config) adapter._mode = "mp" return adapter @pytest.fixture def personal_adapter(self): config = {"bridge_url": "http://localhost:5555", "dm_policy": "open"} adapter = WeChatAdapter(config) adapter._mode = "personal" return adapter def test_format_wecom_direct_message(self, wecom_adapter): response = ChannelResponse( identity=ChannelIdentity( channel_id="wechat", channel_type=ChannelType.WECHAT, channel_user_id="user123", channel_chat_id="user123", ), content="回复消息", metadata={"chat_type": "direct"}, ) payload = wecom_adapter.format_outbound(response) assert payload["msgtype"] == "text" assert payload["touser"] == "user123" assert payload["agentid"] == "1000001" assert payload["text"]["content"] == "回复消息" def test_format_mp_message(self, mp_adapter): response = ChannelResponse( identity=ChannelIdentity( channel_id="wechat", channel_type=ChannelType.WECHAT, channel_user_id="openid_abc", channel_chat_id="openid_abc", ), content="公众号回复", ) payload = mp_adapter.format_outbound(response) assert payload["touser"] == "openid_abc" assert payload["msgtype"] == "text" assert payload["text"]["content"] == "公众号回复" def test_format_bridge_direct_message(self, personal_adapter): response = ChannelResponse( identity=ChannelIdentity( channel_id="wechat", channel_type=ChannelType.WECHAT, channel_user_id="wxid_abc", channel_chat_id="wxid_abc", ), content="桥接回复", metadata={"chat_type": "direct"}, ) payload = personal_adapter.format_outbound(response) assert payload["chat_id"] == "wxid_abc" assert payload["is_group"] is False assert payload["content"] == "桥接回复" def test_format_bridge_group_message(self, personal_adapter): response = ChannelResponse( identity=ChannelIdentity( channel_id="wechat", channel_type=ChannelType.WECHAT, channel_user_id="wxid_abc", channel_chat_id="room_123", ), content="群聊回复", metadata={"chat_type": "group"}, ) payload = personal_adapter.format_outbound(response) assert payload["chat_id"] == "room_123" assert payload["is_group"] is True def test_text_truncation(self, wecom_adapter): long_content = "A" * 3000 response = ChannelResponse( identity=ChannelIdentity( channel_id="wechat", channel_type=ChannelType.WECHAT, channel_user_id="user123", channel_chat_id="user123", ), content=long_content, metadata={"chat_type": "direct"}, ) payload = wecom_adapter.format_outbound(response) assert len(payload["text"]["content"]) <= 2048 class TestSecurity: @pytest.fixture def adapter(self): config = {"dm_policy": "open", "allow_from": ["wx:allowed_user"]} return WeChatAdapter(config) def test_dm_policy_open(self): config = {"dm_policy": "open"} adapter = WeChatAdapter(config) assert adapter._check_dm_policy("any_user") is True def test_dm_policy_disabled(self): config = {"dm_policy": "disabled"} adapter = WeChatAdapter(config) assert adapter._check_dm_policy("any_user") is False def test_dm_policy_allowlist_allowed(self): config = {"dm_policy": "allowlist", "allow_from": ["wx:allowed_user"]} adapter = WeChatAdapter(config) assert adapter._check_dm_policy("allowed_user") is True def test_dm_policy_allowlist_denied(self): config = {"dm_policy": "allowlist", "allow_from": ["wx:other_user"]} adapter = WeChatAdapter(config) assert adapter._check_dm_policy("denied_user") is False def test_group_policy_open(self): config = {"group_policy": "open"} adapter = WeChatAdapter(config) assert adapter._check_group_policy("any_chat", "any_user") is True def test_group_policy_disabled(self): config = {"group_policy": "disabled"} adapter = WeChatAdapter(config) assert adapter._check_group_policy("any_chat", "any_user") is False def test_group_policy_allowlist_global(self): config = { "group_policy": "allowlist", "group_allow_from": ["wx:allowed_user"], } adapter = WeChatAdapter(config) assert adapter._check_group_policy("some_chat", "allowed_user") is True def test_at_bot_wecom(self): adapter = WeChatAdapter({"dm_policy": "open"}) adapter._mode = "wecom" adapter.config["agent_name"] = "MyBot" payload = {"Content": "@MyBot 你好"} assert adapter._is_at_bot(payload) is True def test_at_bot_personal(self): adapter = WeChatAdapter({"dm_policy": "open"}) adapter._mode = "personal" payload = {"at_list": ["wxid_bot"]} assert adapter._is_at_bot(payload) is True class TestHealthCheck: @pytest.fixture async def adapter(self): config = {"corp_id": "test", "corp_secret": "test", "agent_id": "1000001", "dm_policy": "open"} adapter = WeChatAdapter(config) adapter._mode = "wecom" adapter._http_client = AsyncMock() return adapter @pytest.mark.asyncio async def test_health_check_no_client(self): adapter = WeChatAdapter({"dm_policy": "open"}) result = await adapter.health_check() assert result.status == "unhealthy" assert "HTTP client not initialized" in (result.last_error or "") @pytest.mark.asyncio async def test_health_check_wecom_success(self): config = {"corp_id": "test", "corp_secret": "test", "agent_id": "1000001", "dm_policy": "open"} adapter = WeChatAdapter(config) adapter._mode = "wecom" mock_resp = MagicMock() mock_resp.json.return_value = {"access_token": "test_token", "expires_in": 7200} mock_resp.headers = {"content-type": "application/json"} mock_client = AsyncMock() mock_client.get.return_value = mock_resp adapter._http_client = mock_client result = await adapter.health_check() assert result.status == "healthy" class TestPreConnect: @pytest.fixture def adapter(self): config = {"dm_policy": "open"} return WeChatAdapter(config) @pytest.mark.asyncio async def test_pre_connect_wecom(self, adapter): adapter._mode = "wecom" result = await adapter.pre_connect() assert result["status"] == "ready" @pytest.mark.asyncio async def test_pre_connect_mp(self, adapter): adapter._mode = "mp" result = await adapter.pre_connect() assert result["status"] == "pending_scan" class TestCapabilitiesUpdated: def test_tts_voice_enabled(self): from yuxi.channels.adapters.wechat.capabilities import WECHAT_CAPABILITIES assert WECHAT_CAPABILITIES["tts"]["voice"]["enabled"] is True assert WECHAT_CAPABILITIES["tts"]["voice"]["synthesis_target"] == "voice-note" def test_reply_enabled(self): from yuxi.channels.adapters.wechat.capabilities import WECHAT_CAPABILITIES assert WECHAT_CAPABILITIES["reply"] is True def test_group_management_enabled(self): from yuxi.channels.adapters.wechat.capabilities import WECHAT_CAPABILITIES assert WECHAT_CAPABILITIES["groupManagement"] is True class TestReplySupport: @pytest.fixture def wecom_adapter(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter config = {"corp_id": "test", "corp_secret": "test", "agent_id": "1000001", "dm_policy": "open"} adapter = WeChatAdapter(config) adapter._mode = "wecom" adapter._wecom_client = MagicMock() adapter._http_client = AsyncMock() return adapter @pytest.fixture def mp_adapter(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter config = {"app_id": "test", "app_secret": "test", "dm_policy": "open"} adapter = WeChatAdapter(config) adapter._mode = "mp" adapter._mp_client = MagicMock() adapter._http_client = AsyncMock() return adapter @pytest.mark.asyncio async def test_wecom_format_reply_with_context(self, wecom_adapter): response = ChannelResponse( identity=ChannelIdentity( channel_id="wechat", channel_type=ChannelType.WECHAT, channel_user_id="user123", channel_chat_id="user123", ), content="回复消息", reply_to_message_id="msg_456", metadata={"reply_to_channel_user_id": "original_sender"}, ) payload = wecom_adapter.format_outbound(response) assert payload is not None @pytest.mark.asyncio async def test_mp_format_reply_with_context(self, mp_adapter): response = ChannelResponse( identity=ChannelIdentity( channel_id="wechat", channel_type=ChannelType.WECHAT, channel_user_id="openid_abc", channel_chat_id="openid_abc", ), content="回复消息", reply_to_message_id="msg_789", metadata={"reply_to_channel_user_id": "original_sender"}, ) payload = mp_adapter.format_outbound(response) assert payload["touser"] == "openid_abc" def test_normalize_bridge_reply_id(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._mode = "personal" payload = { "sender_id": "wxid_abc", "chat_id": "wxid_abc", "msg_id": "333", "content": "回复", "is_group": False, "msg_type": 1, "reply_to_msg_id": "orig_222", } result = adapter.normalize_inbound(payload) assert result.reply_to_message_id == "orig_222" class TestBannedDetection: @pytest.fixture def adapter(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter config = {"corp_id": "test", "corp_secret": "test", "agent_id": "1000001", "dm_policy": "open"} return WeChatAdapter(config) def test_banned_initial_state(self, adapter): assert adapter.is_banned is False assert adapter.banned_reason is None def test_check_banned_response_48001(self, adapter): result = DeliveryResult(success=False, error="errcode=48001 API unauthorized") adapter._check_banned_response(result) assert adapter.is_banned is True assert "48001" in adapter.banned_reason assert adapter._ban_permanent is False assert adapter._ban_attempts == 1 assert adapter._ban_cooldown_until is not None assert adapter._status.value == "error" def test_check_banned_response_progressive_permanent(self, adapter): adapter._ban_backoff_intervals = [1, 5] result = DeliveryResult(success=False, error="errcode=48001 API unauthorized") adapter._check_banned_response(result) assert adapter._ban_permanent is False assert adapter._ban_attempts == 1 adapter._check_banned_response(result) assert adapter._ban_permanent is False assert adapter._ban_attempts == 2 adapter._check_banned_response(result) assert adapter._ban_permanent is True assert adapter._ban_attempts == 3 assert adapter._status.value == "disabled" def test_unban_channel_temporary(self, adapter): adapter._banned = True adapter._banned_reason = "48001" adapter._ban_permanent = False adapter._ban_cooldown_until = 1000.0 result = adapter.unban_channel() assert result is True assert adapter._banned is False assert adapter._banned_reason is None assert adapter._ban_cooldown_until is None def test_unban_channel_permanent(self, adapter): adapter._banned = True adapter._ban_permanent = True result = adapter.unban_channel() assert result is False assert adapter._banned is True def test_ban_status_property(self, adapter): adapter._banned = True adapter._banned_reason = "48001" adapter._ban_permanent = False adapter._ban_attempts = 1 adapter._ban_cooldown_until = 9999999999.0 status = adapter.ban_status assert status["banned"] is True assert status["permanent"] is False assert status["attempts"] == 1 assert status["cooldown_remaining_seconds"] is not None def test_check_ban_cooldown_expired(self, adapter): import time adapter._banned = True adapter._ban_permanent = False adapter._ban_attempts = 1 adapter._ban_cooldown_until = time.time() - 10 adapter._check_ban_cooldown() assert adapter._banned is False assert adapter._ban_cooldown_until is None class TestAutoJoin: @pytest.fixture def wecom_adapter(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter config = { "corp_id": "test", "corp_secret": "test", "agent_id": "1000001", "dm_policy": "open", "auto_join_groups": ["chat_1", "chat_2"], } adapter = WeChatAdapter(config) adapter._mode = "wecom" adapter._wecom_client = MagicMock() adapter._wecom_client.get_access_token = AsyncMock(return_value="test_token") adapter._http_client = AsyncMock() return adapter @pytest.mark.asyncio async def test_auto_join_groups_wecom_existing(self, wecom_adapter): get_resp = MagicMock() get_resp.json.return_value = {"errcode": 0, "chat_info": {"chatid": "chat_1"}} wecom_adapter._http_client.get = AsyncMock(return_value=get_resp) await wecom_adapter._auto_join_groups() @pytest.mark.asyncio async def test_auto_join_groups_disabled_when_empty(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._mode = "personal" adapter._http_client = AsyncMock() await adapter._auto_join_groups() class TestAutoDerive: def test_auto_derive_disabled_by_default(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) assert adapter._should_auto_derive() is False def test_auto_derive_enabled_with_config(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter config = { "bridge_url": "http://localhost:5555", "dm_policy": "open", "auto_derive_sessions": True, } adapter = WeChatAdapter(config) assert adapter._should_auto_derive() is True def test_auto_derive_key_format(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"}) adapter._mode = "personal" key = adapter._get_auto_derive_key("room_123", "sender_abc") assert "personal" in key assert "room_123" in key assert "sender_abc" in key def test_auto_derive_metadata_in_bridge_group(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter config = { "bridge_url": "http://localhost:5555", "dm_policy": "open", "auto_derive_sessions": True, } adapter = WeChatAdapter(config) adapter._mode = "personal" payload = { "sender_id": "wxid_abc", "chat_id": "room_123", "msg_id": "555", "content": "群聊消息", "is_group": True, "msg_type": 1, } result = adapter.normalize_inbound(payload) assert result.metadata.get("auto_derive_session") is True assert "auto_derive_key" in result.metadata class TestLogoutAccount: @pytest.fixture def adapter(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter config = {"corp_id": "test", "corp_secret": "test", "agent_id": "1000001", "dm_policy": "open"} adapter = WeChatAdapter(config) adapter._mode = "wecom" adapter._wecom_client = MagicMock() adapter._wecom_client.invalidate_token = MagicMock() return adapter @pytest.mark.asyncio async def test_logout_clears_wecom_client(self, adapter): await adapter.logout_account() assert adapter._wecom_client is None @pytest.mark.asyncio async def test_logout_returns_result(self, adapter): result = await adapter.logout_account() assert "cleared" in result assert "loggedOut" in result class TestReadMessage: @pytest.mark.asyncio async def test_read_message_wecom(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"corp_id": "test", "corp_secret": "test", "agent_id": "1000001", "dm_policy": "open"}) adapter._mode = "wecom" adapter._wecom_client = MagicMock() adapter._wecom_client.get_access_token = AsyncMock(return_value="token") adapter._http_client = AsyncMock() mock_head_resp = MagicMock() mock_head_resp.status_code = 200 adapter._http_client.head = AsyncMock(return_value=mock_head_resp) result = await adapter.read_message("media_123") assert result["msg_id"] == "media_123" assert result["available"] is True assert "read_at" in result @pytest.mark.asyncio async def test_read_message_unavailable(self): from yuxi.channels.adapters.wechat.adapter import WeChatAdapter adapter = WeChatAdapter({"app_id": "test", "app_secret": "test", "dm_policy": "open"}) adapter._mode = "mp" adapter._mp_client = MagicMock() adapter._mp_client.get_access_token = AsyncMock(return_value="token") adapter._http_client = AsyncMock() mock_head_resp = MagicMock() mock_head_resp.status_code = 404 adapter._http_client.head = AsyncMock(return_value=mock_head_resp) result = await adapter.read_message("expired_media") assert result["msg_id"] == "expired_media" assert result["available"] is False def test_read_action_is_implemented(self): from yuxi.channels.adapters.wechat.message_actions import WECHAT_MESSAGE_ACTIONS assert WECHAT_MESSAGE_ACTIONS["read"]["status"] == "implemented" class TestConnectionLifecycle: @pytest.fixture def wecom_config(self): return { "corp_id": "test_corp", "corp_secret": "test_secret", "agent_id": "1000001", "dm_policy": "open", } @pytest.fixture def personal_config(self): return { "bridge_url": "http://localhost:5555", "dm_policy": "open", } def test_initial_status_disconnected(self): adapter = WeChatAdapter({"dm_policy": "open"}) assert adapter._status == ChannelStatus.DISCONNECTED def test_connect_twice_idempotent(self, wecom_config): adapter = WeChatAdapter(wecom_config) adapter._mode = "wecom" adapter._status = ChannelStatus.CONNECTED @pytest.mark.asyncio async def test_disconnect_cleans_up(self, wecom_config): adapter = WeChatAdapter(wecom_config) adapter._mode = "wecom" adapter._http_client = AsyncMock() adapter._status = ChannelStatus.CONNECTED await adapter.disconnect() assert adapter._status == ChannelStatus.DISCONNECTED assert adapter._http_client is None @pytest.mark.asyncio async def test_disconnect_already_disconnected(self): adapter = WeChatAdapter({"dm_policy": "open"}) adapter._status = ChannelStatus.DISCONNECTED await adapter.disconnect() assert adapter._status == ChannelStatus.DISCONNECTED @pytest.mark.asyncio async def test_disconnect_cancels_polling_task(self, personal_config): adapter = WeChatAdapter(personal_config) adapter._mode = "personal" adapter._http_client = AsyncMock() adapter._status = ChannelStatus.CONNECTED async def mock_loop(_interval): try: while True: await asyncio.sleep(10) except asyncio.CancelledError: raise adapter._polling_task = asyncio.create_task(mock_loop(1.0)) await asyncio.sleep(0.01) await adapter.disconnect() assert adapter._polling_task is None assert adapter._status == ChannelStatus.DISCONNECTED @pytest.mark.asyncio async def test_pre_connect_wecom(self): adapter = WeChatAdapter({"corp_id": "test", "corp_secret": "test", "agent_id": "1", "dm_policy": "open"}) adapter._mode = "wecom" result = await adapter.pre_connect() assert result["status"] == "ready" assert "token" in result["message"].lower() @pytest.mark.asyncio async def test_pre_connect_mp(self): adapter = WeChatAdapter({"app_id": "test", "app_secret": "test", "dm_policy": "open"}) adapter._mode = "mp" result = await adapter.pre_connect() assert result["status"] == "pending_scan" @pytest.mark.asyncio async def test_pre_connect_unknown_mode(self): adapter = WeChatAdapter({"dm_policy": "open"}) adapter._mode = "personal" result = await adapter.pre_connect() assert result["status"] in ("error", "pending_scan") @pytest.mark.asyncio async def test_login_with_qr_wait_wecom(self): adapter = WeChatAdapter({"corp_id": "test", "corp_secret": "test", "agent_id": "1", "dm_policy": "open"}) adapter._mode = "wecom" result = await adapter.login_with_qr_wait() assert result["status"] == "ready" @pytest.mark.asyncio async def test_login_with_qr_wait_mp(self): adapter = WeChatAdapter({"app_id": "test", "app_secret": "test", "dm_policy": "open"}) adapter._mode = "mp" result = await adapter.login_with_qr_wait() assert result["status"] == "ready" def test_state_snapshot_disconnected(self): adapter = WeChatAdapter({"dm_policy": "open"}) snapshot = adapter.state_snapshot assert snapshot.configured is True assert snapshot.connected is False def test_resolve_config_from_env(self, monkeypatch): monkeypatch.setenv("WECHAT_BRIDGE_URL", "http://bridge.example.com") adapter = WeChatAdapter({"dm_policy": "open"}) value = adapter._resolve_config("bridge_url", "WECHAT_BRIDGE_URL") assert value == "http://bridge.example.com" def test_resolve_config_from_config_first(self): adapter = WeChatAdapter({"bridge_url": "http://cfg.example.com", "dm_policy": "open"}) value = adapter._resolve_config("bridge_url", "WECHAT_BRIDGE_URL") assert value == "http://cfg.example.com" def test_logout_clears_all_clients(self): adapter = WeChatAdapter({"corp_id": "test", "corp_secret": "test", "agent_id": "1", "dm_policy": "open"}) adapter._mode = "wecom" adapter._wecom_client = MagicMock() adapter._mp_client = MagicMock() adapter._bridge_client = MagicMock() import asyncio loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: result = loop.run_until_complete(adapter.logout_account()) assert adapter._wecom_client is None assert adapter._mp_client is None assert adapter._bridge_client is None finally: loop.close()