from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch import pytest from yuxi.channels.adapters.yuanbao.adapter import YuanbaoAdapter from yuxi.channels.adapters.yuanbao.dispatch import ( DispatchAction, InteractiveDispatcher, ) from yuxi.channels.adapters.yuanbao.format import format_outbound, _map_message_type from yuxi.channels.models import ( ChannelIdentity, ChannelMessage, ChannelResponse, ChannelType, ChatType, DeliveryResult, MessageType, ) YUANBAO = ChannelType.YUANBAO def _make_identity( channel_chat_id: str = "user_openid_001", channel_user_id: str = "user_openid_001", channel_message_id: str = "msg_001", ) -> ChannelIdentity: return ChannelIdentity( channel_id="yuanbao", channel_type=YUANBAO, channel_user_id=channel_user_id, channel_chat_id=channel_chat_id, channel_message_id=channel_message_id, ) def _make_adapter(config: dict | None = None) -> YuanbaoAdapter: cfg = config or {"app_key": "test_key", "app_secret": "test_secret", "dm_policy": "open"} return YuanbaoAdapter(config=cfg) # ==================== ChannelCapabilities Tests ==================== class TestChannelCapabilitiesOptimization: def test_capabilities_has_explicit_false_fields(self): adapter = _make_adapter() caps = adapter.capabilities assert caps.edit is False assert caps.unsend is False assert caps.pin is False assert caps.polls is False assert caps.typing is False assert caps.send_ephemeral is False assert caps.effects is False def test_capabilities_retains_true_fields(self): adapter = _make_adapter() caps = adapter.capabilities assert caps.reply is True assert caps.reactions is True assert caps.media is True assert caps.block_streaming is True assert caps.lane_streaming is True assert caps.reasoning_streaming is True assert caps.native_commands is True assert caps.supports_markdown is True assert caps.supports_streaming is True def test_capabilities_chat_types(self): adapter = _make_adapter() caps = adapter.capabilities assert "direct" in caps.chat_types assert "group" in caps.chat_types def test_capabilities_streaming_modes(self): adapter = _make_adapter() caps = adapter.capabilities assert "off" in caps.streaming_modes assert "block" in caps.streaming_modes assert "lane" in caps.streaming_modes assert "reasoning" in caps.streaming_modes # ==================== send_typing_indicator Tests ==================== class TestSendTypingIndicator: @pytest.fixture def adapter(self): return _make_adapter() @pytest.mark.asyncio async def test_typing_indicator_no_client(self, adapter): result = await adapter.send_typing_indicator("chat_001") assert result.success is False assert "not initialized" in result.error.lower() @pytest.mark.asyncio async def test_typing_indicator_success(self, adapter): adapter._http_client = MagicMock() adapter._token_manager = MagicMock() adapter._token_manager.get_token = AsyncMock(return_value="test_token") adapter._token_manager.api_base = "https://api.yuanbao.test" mock_resp = AsyncMock() mock_resp.status = 200 mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) mock_resp.__aexit__ = AsyncMock(return_value=None) adapter._http_client.post.return_value = mock_resp result = await adapter.send_typing_indicator("open_id_001") assert result.success is True @pytest.mark.asyncio async def test_typing_indicator_http_error(self, adapter): adapter._http_client = MagicMock() adapter._token_manager = MagicMock() adapter._token_manager.get_token = AsyncMock(return_value="test_token") adapter._token_manager.api_base = "https://api.yuanbao.test" mock_resp = AsyncMock() mock_resp.status = 500 mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) mock_resp.__aexit__ = AsyncMock(return_value=None) adapter._http_client.post.return_value = mock_resp result = await adapter.send_typing_indicator("open_id_001") assert result.success is False assert "500" in result.error @pytest.mark.asyncio async def test_typing_indicator_network_exception(self, adapter): adapter._http_client = MagicMock() adapter._token_manager = MagicMock() adapter._token_manager.get_token = AsyncMock(return_value="test_token") adapter._token_manager.api_base = "https://api.yuanbao.test" adapter._http_client.post.side_effect = Exception("Connection refused") result = await adapter.send_typing_indicator("open_id_001") assert result.success is False assert "Connection refused" in result.error # ==================== edit_message / delete_message Tests ==================== class TestEditDeleteNoSideEffects: @pytest.fixture def adapter(self): return _make_adapter() @pytest.mark.asyncio async def test_edit_message_no_cache_side_effect(self, adapter): adapter._send_cache.add("msg_abc", "chat_123", "Original content") result = await adapter.edit_message("chat_123", "msg_abc", "Updated content") assert result.success is False assert "not supported" in result.error.lower() entry = adapter._send_cache.get("msg_abc") assert entry is not None assert entry.status == "sent" @pytest.mark.asyncio async def test_delete_message_no_cache_side_effect(self, adapter): adapter._send_cache.add("msg_abc", "chat_123", "Original content") adapter._bot_message_ids.add("msg_abc") result = await adapter.delete_message("chat_123", "msg_abc") assert result.success is False assert "not supported" in result.error.lower() entry = adapter._send_cache.get("msg_abc") assert entry is not None assert entry.status == "sent" assert "msg_abc" in adapter._bot_message_ids # ==================== SYSTEM_EVENT Handler Tests ==================== class TestSystemEventHandler: @pytest.fixture def adapter(self): return _make_adapter() @pytest.mark.asyncio async def test_handle_system_event_typing(self, adapter): event = { "type": "typing", "open_id": "user_123", "group_open_id": "group_abc", } await adapter._handle_system_event(event) @pytest.mark.asyncio async def test_handle_system_event_unknown(self, adapter): event = { "type": "some_custom_event", "open_id": "user_123", } await adapter._handle_system_event(event) @pytest.mark.asyncio async def test_handle_ws_event_dispatches_system_event(self, adapter): event = { "type": "typing", "msg_id": "msg_typing_001", "open_id": "user_123", } adapter._seen_message_ids.clear() adapter._handle_system_event = AsyncMock() await adapter._handle_ws_event(event) adapter._handle_system_event.assert_called_once_with(event) # ==================== LOCATION Type Mapping Tests ==================== class TestLocationTypeMapping: def test_map_message_type_includes_location(self): result = _map_message_type(MessageType.LOCATION, "") assert result == "location" def test_format_outbound_with_location_type(self): identity = _make_identity(channel_chat_id="user_001") response = ChannelResponse( identity=identity, content="Location share", message_type=MessageType.LOCATION, ) payload = format_outbound(response) assert payload["msg_type"] == "location" assert payload["content"] == "Location share" # ==================== Dispatch Classification Tests ==================== class TestDispatchClassificationOptimization: def test_channel_post_classified_as_message(self): event = { "type": "channel_post", "open_id": "user_123", "channel_id": "ch_001", "content": "Channel broadcast", } action = InteractiveDispatcher.classify_event(event) assert action == DispatchAction.MESSAGE def test_edited_channel_post_classified_as_message(self): event = { "type": "edited_channel_post", "open_id": "user_123", "channel_id": "ch_001", "content": "Edited channel broadcast", } action = InteractiveDispatcher.classify_event(event) assert action == DispatchAction.MESSAGE def test_normal_message_still_classified_correctly(self): event = { "type": "message", "open_id": "user_123", "content": "Hello", } action = InteractiveDispatcher.classify_event(event) assert action == DispatchAction.MESSAGE def test_command_still_classified_correctly(self): event = { "type": "message", "open_id": "user_123", "content": "/start", } action = InteractiveDispatcher.classify_event(event) assert action == DispatchAction.COMMAND def test_edited_message_still_classified_correctly(self): event = { "type": "edited_message", "open_id": "user_123", "content": "Updated", } action = InteractiveDispatcher.classify_event(event) assert action == DispatchAction.MESSAGE def test_reaction_still_classified_correctly(self): event = {"type": "reaction_added", "open_id": "user_123"} action = InteractiveDispatcher.classify_event(event) assert action == DispatchAction.REACTION def test_bot_menu_still_classified_correctly(self): event = {"type": "bot_menu", "open_id": "user_123"} action = InteractiveDispatcher.classify_event(event) assert action == DispatchAction.BOT_MENU def test_member_event_still_classified_correctly(self): event = {"type": "member_joined", "open_id": "user_123"} action = InteractiveDispatcher.classify_event(event) assert action == DispatchAction.MEMBER_EVENT def test_card_action_still_classified_correctly(self): event = {"type": "card_action", "open_id": "user_123"} action = InteractiveDispatcher.classify_event(event) assert action == DispatchAction.CARD_ACTION def test_read_receipt_still_classified_correctly(self): event = {"type": "read_receipt", "open_id": "user_123"} action = InteractiveDispatcher.classify_event(event) assert action == DispatchAction.READ_RECEIPT def test_unknown_event_type(self): event = {"type": "unknown_type", "open_id": "user_123"} action = InteractiveDispatcher.classify_event(event) assert action == DispatchAction.UNKNOWN