from __future__ import annotations import asyncio import json import time from unittest.mock import AsyncMock, MagicMock, patch import pytest from yuxi.channels.adapters.yuanbao.adapter import YuanbaoAdapter from yuxi.channels.adapters.yuanbao.chunking import chunk_text from yuxi.channels.adapters.yuanbao.config_mapper import normalize_config, to_openclaw_key from yuxi.channels.adapters.yuanbao.directory import ( DirectoryResult, GroupInfo, PeerInfo, ) from yuxi.channels.adapters.yuanbao.dispatch import ( BotMenuAction, CardAction, DispatchAction, DispatchContext, DispatchResult, InteractiveDispatcher, ) from yuxi.channels.adapters.yuanbao.format import format_outbound from yuxi.channels.adapters.yuanbao.event_queue import EventQueue from yuxi.channels.adapters.yuanbao.outbound_queue import OutboundQueue from yuxi.channels.adapters.yuanbao.probe import health_check_yuanbao from yuxi.channels.adapters.yuanbao.proto_codec import ProtoCodec, is_protobuf_message from yuxi.channels.adapters.yuanbao.security import ( check_dm_policy, check_group_policy, check_mention_required, ) from yuxi.channels.adapters.yuanbao.security_audit import SecurityAuditLogger from yuxi.channels.adapters.yuanbao.send import send_with_retry from yuxi.channels.adapters.yuanbao.send_cache import SendMessageCache, SentMessageEntry from yuxi.channels.adapters.yuanbao.session import ( resolve_agent_route, resolve_chat_type_str, resolve_thread_key, ) from yuxi.channels.adapters.yuanbao.streaming import ( LaneStreamManager, ReasoningStreamManager, StreamManager, StreamMode, create_stream_manager, send_blocks_stream, ) from yuxi.channels.adapters.yuanbao.template import ( ActionSelector, SelectorOption, TemplateButton, TemplateCard, TemplateMessageBuilder, ) from yuxi.channels.adapters.yuanbao.token import YuanbaoTokenManager from yuxi.channels.adapters.yuanbao.yb_accounts import ( YuanbaoAccount, YuanbaoAccountManager, load_accounts_from_config, ) from yuxi.channels.adapters.yuanbao.yb_commands import ( COMMAND_DEFINITIONS, NativeCommandContext, handle_command, parse_command, sync_commands_menu, ) from yuxi.channels.adapters.yuanbao.doc_gen import YUANBAO_CONFIG_KEYS, generate_channel_docs from yuxi.channels.capabilities import ChannelCapabilities from yuxi.channels.meta import ChannelMeta from yuxi.channels.models import ( Attachment, ChannelIdentity, ChannelMessage, ChannelResponse, ChannelType, ChatType, DeliveryResult, EventType, MessageType, MentionsInfo, ) 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_response( content: str = "Hello from Yuanbao", channel_chat_id: str = "user_openid_001", **kwargs, ) -> ChannelResponse: identity = _make_identity(channel_chat_id=channel_chat_id) return ChannelResponse(identity=identity, content=content, **kwargs) # ==================== Token Manager Tests ==================== class TestYuanbaoTokenManager: def test_token_initial_state(self): tm = YuanbaoTokenManager(app_key="test_key", app_secret="test_secret", bot_app_id="bot_001") assert tm._access_token is None assert tm._is_expired() is True def test_api_base_default(self): tm = YuanbaoTokenManager(app_key="test_key", app_secret="test_secret", bot_app_id="bot_001") assert tm.api_base == "https://open-api.yuanbao.tencent.com" def test_api_base_custom(self): tm = YuanbaoTokenManager(app_key="test_key", app_secret="test_secret", bot_app_id="bot_001", api_base="https://custom.api.com") assert tm.api_base == "https://custom.api.com" def test_bot_app_id(self): tm = YuanbaoTokenManager(app_key="test_key", app_secret="test_secret", bot_app_id="my_bot") assert tm.bot_app_id == "my_bot" def test_sign_request(self): tm = YuanbaoTokenManager(app_key="test_key", app_secret="test_secret", bot_app_id="bot_001") signature = tm._sign_request(1700000000) assert isinstance(signature, str) assert len(signature) == 64 def test_sign_request_deterministic(self): tm = YuanbaoTokenManager(app_key="test_key", app_secret="test_secret", bot_app_id="bot_001") sig1 = tm._sign_request(1700000000) sig2 = tm._sign_request(1700000000) assert sig1 == sig2 def test_is_expired_with_no_token(self): tm = YuanbaoTokenManager(app_key="test_key", app_secret="test_secret", bot_app_id="bot_001") assert tm._is_expired() is True def test_is_expired_with_no_expires_at(self): tm = YuanbaoTokenManager(app_key="test_key", app_secret="test_secret", bot_app_id="bot_001") tm._access_token = "token" tm._expires_at = None assert tm._is_expired() is True def test_is_expired_future_token(self): tm = YuanbaoTokenManager(app_key="test_key", app_secret="test_secret", bot_app_id="bot_001") tm._access_token = "token" tm._expires_at = time.time() + 7200 assert tm._is_expired() is False def test_is_expired_expired_token(self): tm = YuanbaoTokenManager(app_key="test_key", app_secret="test_secret", bot_app_id="bot_001") tm._access_token = "old" tm._expires_at = time.time() - 100 assert tm._is_expired() is True def test_pre_signed_token_initial(self): tm = YuanbaoTokenManager(app_key="test_key", app_secret="test_secret", bot_app_id="bot_001", pre_signed_token="presigned") assert tm._access_token == "presigned" @pytest.mark.asyncio async def test_get_token_returns_pre_signed(self): tm = YuanbaoTokenManager(app_key="test_key", app_secret="test_secret", bot_app_id="bot_001", pre_signed_token="presigned") token = await tm.get_token() assert token == "presigned" @pytest.mark.asyncio async def test_get_token_refreshes(self): tm = YuanbaoTokenManager(app_key="test_key", app_secret="test_secret", bot_app_id="bot_001") tm._access_token = None tm._expires_at = None with patch.object(tm, "_refresh", new_callable=AsyncMock) as mock_refresh: mock_refresh.return_value = None await tm.get_token() mock_refresh.assert_called_once() @pytest.mark.asyncio async def test_get_token_not_expired(self): tm = YuanbaoTokenManager(app_key="test_key", app_secret="test_secret", bot_app_id="bot_001") tm._access_token = "valid" tm._expires_at = time.time() + 7200 token = await tm.get_token() assert token == "valid" @pytest.mark.asyncio async def test_refresh_token(self): tm = YuanbaoTokenManager(app_key="test_key", app_secret="test_secret", bot_app_id="bot_001") with patch.object(tm, "get_token", new_callable=AsyncMock) as mock_get: mock_get.return_value = "new_token" result = await tm.refresh_token() assert result == "new_token" # ==================== Format Tests ==================== class TestFormatOutbound: def test_text_payload_direct(self): resp = _make_response(content="Hello World", channel_chat_id="user_openid_001") payload = format_outbound(resp) assert payload["content"] == "Hello World" assert payload["msg_type"] == "text" assert payload["open_id"] == "user_openid_001" def test_text_payload_group(self): resp = _make_response(content="Group message", channel_chat_id="group_openid_abc") resp.metadata = {"group_open_id": "group_openid_abc"} payload = format_outbound(resp) assert payload["content"] == "Group message" assert payload["group_open_id"] == "group_openid_abc" def test_text_payload_channel(self): resp = _make_response(content="Channel message", channel_chat_id="ch_001") resp.metadata = {"channel_id": "ch_001"} payload = format_outbound(resp) assert payload["content"] == "Channel message" assert payload["channel_id"] == "ch_001" def test_image_payload(self): resp = _make_response(content="Image caption", channel_chat_id="user_001") resp.message_type = MessageType.IMAGE resp.attachments = [Attachment(type="image", url="https://example.com/img.png")] payload = format_outbound(resp) assert payload["msg_type"] == "image" assert payload["media_url"] == "https://example.com/img.png" def test_image_payload_no_attachment(self): resp = _make_response(content="Image caption", channel_chat_id="user_001") resp.message_type = MessageType.IMAGE payload = format_outbound(resp) assert payload["msg_type"] == "image" assert payload["content"] == "Image caption" assert "media_url" not in payload def test_file_payload(self): resp = _make_response(content="File description", channel_chat_id="user_001") resp.message_type = MessageType.FILE resp.attachments = [Attachment(type="file", url="https://example.com/doc.pdf", filename="doc.pdf")] payload = format_outbound(resp) assert payload["msg_type"] == "file" assert payload["media_url"] == "https://example.com/doc.pdf" assert payload["filename"] == "doc.pdf" def test_file_payload_no_filename(self): resp = _make_response(content="File", channel_chat_id="user_001") resp.message_type = MessageType.FILE resp.attachments = [Attachment(type="file", url="https://example.com/doc.pdf")] payload = format_outbound(resp) assert payload["filename"] == "file" def test_payload_with_reply(self): resp = _make_response(content="Reply", reply_to_message_id="msg_orig") payload = format_outbound(resp) assert payload["reply_to_msg_id"] == "msg_orig" def test_audio_as_file(self): resp = _make_response(content="Audio file", channel_chat_id="user_001") resp.message_type = MessageType.AUDIO payload = format_outbound(resp) assert payload["msg_type"] == "audio" def test_extra_metadata(self): resp = _make_response(content="Extra", channel_chat_id="user_001") resp.metadata = {"extra": {"key": "value"}} payload = format_outbound(resp) assert payload["extra"] == {"key": "value"} def test_sticker_payload(self): resp = _make_response(content="sticker", channel_chat_id="user_001") resp.message_type = MessageType.STICKER resp.attachments = [Attachment(type="image", url="https://example.com/sticker.png")] payload = format_outbound(resp) assert payload["msg_type"] == "sticker" assert payload["media_url"] == "https://example.com/sticker.png" def test_video_payload(self): resp = _make_response(content="video", channel_chat_id="user_001") resp.message_type = MessageType.VIDEO payload = format_outbound(resp) assert payload["msg_type"] == "video" def test_card_payload(self): resp = _make_response(content="card", channel_chat_id="user_001") resp.message_type = MessageType.CARD payload = format_outbound(resp) assert payload["msg_type"] == "card" def test_buttons_in_metadata(self): resp = _make_response(content="Buttons", channel_chat_id="user_001") resp.metadata = {"buttons": [{"text": "Click", "type": "url", "value": "https://example.com"}]} payload = format_outbound(resp) assert payload["buttons"] == [{"text": "Click", "type": "url", "value": "https://example.com"}] def test_card_in_metadata(self): resp = _make_response(content="Card", channel_chat_id="user_001") resp.metadata = {"card": {"type": "card", "title": "Test Card"}} payload = format_outbound(resp) assert payload["card"] == {"type": "card", "title": "Test Card"} def test_empty_chat_id_raises(self): resp = _make_response(content="test", channel_chat_id="") with pytest.raises(ValueError, match="channel_chat_id is empty"): format_outbound(resp) def test_metadata_none(self): resp = _make_response(content="test", channel_chat_id="user_001") resp.metadata = None payload = format_outbound(resp) assert payload["content"] == "test" def test_group_open_id_takes_priority(self): resp = _make_response(content="test", channel_chat_id="user_001") resp.metadata = {"group_open_id": "group_abc", "channel_id": "ch_001"} payload = format_outbound(resp) assert "group_open_id" in payload assert "channel_id" not in payload # ==================== Normalize Inbound Tests ==================== class TestNormalizeInbound: @pytest.fixture def adapter(self): config = {"app_key": "test_key", "app_secret": "test_secret", "dm_policy": "open"} return YuanbaoAdapter(config=config) def test_direct_message(self, adapter): raw = { "type": "message", "open_id": "user_123", "msg_id": "msg_001", "msg_type": "text", "content": "\u4f60\u597d", "chat_type": "direct", "timestamp": 1700000000, } msg = adapter.normalize_inbound(raw) assert msg.identity.channel_chat_id == "user_123" assert msg.chat_type == ChatType.DIRECT assert msg.content == "\u4f60\u597d" assert msg.message_type == MessageType.TEXT def test_group_message(self, adapter): raw = { "type": "message", "open_id": "user_456", "group_open_id": "group_abc", "msg_id": "msg_002", "msg_type": "text", "content": "\u7fa4\u804a\u6d88\u606f", "chat_type": "group", "timestamp": 1700000000, } msg = adapter.normalize_inbound(raw) assert msg.identity.channel_chat_id == "group_abc" assert msg.chat_type == ChatType.GROUP assert msg.metadata.get("group_open_id") == "group_abc" def test_channel_message(self, adapter): raw = { "type": "message", "open_id": "user_789", "channel_id": "ch_guild_001", "msg_id": "msg_003", "msg_type": "text", "content": "\u9891\u9053\u6d88\u606f", "chat_type": "channel", "timestamp": 1700000000, } msg = adapter.normalize_inbound(raw) assert msg.identity.channel_chat_id == "ch_guild_001" assert msg.chat_type == ChatType.GUILD_CHANNEL assert msg.metadata.get("channel_id") == "ch_guild_001" def test_image_message(self, adapter): raw = { "type": "message", "open_id": "user_123", "msg_id": "msg_004", "msg_type": "image", "content": "", "chat_type": "direct", "timestamp": 1700000000, "attachments": [ {"type": "image", "url": "https://example.com/photo.jpg", "filename": "photo.jpg", "size": 102400} ], } msg = adapter.normalize_inbound(raw) assert msg.message_type == MessageType.IMAGE assert len(msg.attachments) == 1 assert msg.attachments[0].url == "https://example.com/photo.jpg" def test_file_message(self, adapter): raw = { "type": "message", "open_id": "user_123", "msg_id": "msg_005", "msg_type": "file", "content": "\u6587\u6863", "chat_type": "direct", "timestamp": 1700000000, "attachments": [ {"type": "file", "url": "https://example.com/doc.pdf", "filename": "doc.pdf", "size": 204800} ], } msg = adapter.normalize_inbound(raw) assert msg.message_type == MessageType.FILE assert len(msg.attachments) == 1 def test_markdown_message(self, adapter): raw = { "type": "message", "open_id": "user_123", "msg_id": "msg_006", "msg_type": "markdown", "content": "**Bold** text", "chat_type": "direct", "timestamp": 1700000000, } msg = adapter.normalize_inbound(raw) assert msg.message_type == MessageType.TEXT assert msg.content == "**Bold** text" def test_command_detection(self, adapter): raw = { "type": "message", "open_id": "user_123", "msg_id": "msg_007", "msg_type": "text", "content": "/reset", "chat_type": "direct", "timestamp": 1700000000, } msg = adapter.normalize_inbound(raw) assert msg.message_type == MessageType.COMMAND def test_metadata_includes_yuanbao_chat_type(self, adapter): raw = { "type": "message", "open_id": "user_123", "msg_id": "msg_008", "msg_type": "text", "content": "Hello", "chat_type": "direct", "reply_to_msg_id": "msg_prev", "timestamp": 1700000000, } msg = adapter.normalize_inbound(raw) assert msg.metadata["yuanbao_chat_type"] == "direct" assert msg.metadata["reply_to_msg_id"] == "msg_prev" def test_group_message_mention(self, adapter): adapter._bot_info = {"username": "mybot"} raw = { "type": "message", "open_id": "user_456", "group_open_id": "group_abc", "msg_id": "msg_009", "msg_type": "text", "content": "@mybot \u4f60\u597d", "chat_type": "group", "timestamp": 1700000000, } msg = adapter.normalize_inbound(raw) assert msg.mentions is not None assert msg.mentions.is_bot_mentioned is True def test_sticker_message(self, adapter): raw = { "type": "message", "open_id": "user_123", "msg_id": "msg_sticker", "msg_type": "sticker", "content": "", "chat_type": "direct", "timestamp": 1700000000, } msg = adapter.normalize_inbound(raw) assert msg.message_type == MessageType.STICKER def test_edited_message_event_type(self, adapter): raw = { "type": "edited_message", "open_id": "user_123", "msg_id": "msg_edit", "msg_type": "text", "content": "edited", "chat_type": "direct", "timestamp": 1700000000, } msg = adapter.normalize_inbound(raw) assert msg.event_type == EventType.MESSAGE_UPDATED def test_deleted_message_event_type(self, adapter): raw = { "type": "deleted_message", "open_id": "user_123", "msg_id": "msg_del", "msg_type": "text", "content": "", "chat_type": "direct", "timestamp": 1700000000, } msg = adapter.normalize_inbound(raw) assert msg.event_type == EventType.MESSAGE_DELETED def test_no_open_id(self, adapter): raw = { "type": "message", "msg_id": "msg_no_user", "msg_type": "text", "content": "Hello", "chat_type": "direct", "timestamp": 1700000000, } msg = adapter.normalize_inbound(raw) assert msg.identity.channel_user_id == "" def test_invalid_timestamp(self, adapter): raw = { "type": "message", "open_id": "user_123", "msg_id": "msg_bad_ts", "msg_type": "text", "content": "Hello", "chat_type": "direct", "timestamp": "not_a_number", } msg = adapter.normalize_inbound(raw) assert msg.content == "Hello" def test_private_from_group_code(self, adapter): raw = { "type": "message", "open_id": "user_123", "msg_id": "msg_private", "msg_type": "text", "content": "Private", "chat_type": "direct", "private_from_group_code": "group_code_123", "timestamp": 1700000000, } msg = adapter.normalize_inbound(raw) assert msg.metadata["private_from_group_code"] == "group_code_123" def test_custom_elements_link_card(self, adapter): raw = { "type": "message", "open_id": "user_123", "msg_id": "msg_link_card", "msg_type": "text", "content": "", "chat_type": "direct", "timestamp": 1700000000, "custom_elements": [ { "type": "link_card", "url": "https://example.com/article", "title": "Article Title", "description": "Article Description", } ], } msg = adapter.normalize_inbound(raw) assert msg.content == "Article Title" assert msg.extracted_urls == ["https://example.com/article"] assert msg.metadata["link_card"]["url"] == "https://example.com/article" def test_group_no_bot_info_no_mention(self, adapter): raw = { "type": "message", "open_id": "user_456", "group_open_id": "group_abc", "msg_id": "msg_nobot", "msg_type": "text", "content": "Hello", "chat_type": "group", "timestamp": 1700000000, } msg = adapter.normalize_inbound(raw) assert msg.mentions is not None assert msg.mentions.is_bot_mentioned is False # ==================== Security Tests ==================== class TestSecurityPolicy: @pytest.mark.asyncio async def test_dm_policy_open(self): config = {"dm_policy": "open"} result = await check_dm_policy("user_001", config) assert result is True @pytest.mark.asyncio async def test_dm_policy_disabled(self): config = {"dm_policy": "disabled"} result = await check_dm_policy("user_001", config) assert result is False @pytest.mark.asyncio async def test_dm_policy_allowlist_match(self): config = {"dm_policy": "allowlist", "allow_from": ["yb:user_001", "yb:user_002"]} result = await check_dm_policy("yb:user_001", config) assert result is True @pytest.mark.asyncio async def test_dm_policy_allowlist_no_match(self): config = {"dm_policy": "allowlist", "allow_from": ["yb:user_002"]} result = await check_dm_policy("user_001", config) assert result is False @pytest.mark.asyncio async def test_dm_policy_allowlist_wildcard(self): config = {"dm_policy": "allowlist", "allow_from": ["*"]} result = await check_dm_policy("user_anyone", config) assert result is True @pytest.mark.asyncio async def test_dm_policy_unknown(self): config = {"dm_policy": "unknown_policy"} result = await check_dm_policy("user_001", config) assert result is False @pytest.mark.asyncio async def test_dm_policy_nested_config(self): config = {"dm": {"policy": "open"}} result = await check_dm_policy("user_001", config) assert result is True @pytest.mark.asyncio async def test_dm_policy_nested_disabled(self): config = {"dm": {"policy": "disabled"}} result = await check_dm_policy("user_001", config) assert result is False @pytest.mark.asyncio async def test_dm_policy_nested_allowlist(self): config = {"dm": {"policy": "allowlist", "allowFrom": ["yb:user_001"]}} result = await check_dm_policy("yb:user_001", config) assert result is True @pytest.mark.asyncio async def test_dm_policy_pairing_true(self): config = {"dm_policy": "pairing", "paired_users": ["user_001"]} result = await check_dm_policy("user_001", config) assert result is True @pytest.mark.asyncio async def test_dm_policy_pairing_false(self): config = {"dm_policy": "pairing", "paired_users": []} result = await check_dm_policy("user_001", config) assert result is False @pytest.mark.asyncio async def test_dm_policy_default_pairing(self): config = {} result = await check_dm_policy("user_001", config) assert result is False @pytest.mark.asyncio async def test_group_policy_open(self): config = {"group_policy": "open"} result = await check_group_policy("group_abc", "user_001", config) assert result is True @pytest.mark.asyncio async def test_group_policy_disabled(self): config = {"group_policy": "disabled"} result = await check_group_policy("group_abc", "user_001", config) assert result is False @pytest.mark.asyncio async def test_group_policy_allowlist_global(self): config = {"group_policy": "allowlist", "group_allow_from": ["yb:user_001"]} result = await check_group_policy("group_abc", "yb:user_001", config) assert result is True @pytest.mark.asyncio async def test_group_policy_allowlist_global_wildcard(self): config = {"group_policy": "allowlist", "group_allow_from": ["*"]} result = await check_group_policy("group_abc", "user_anyone", config) assert result is True @pytest.mark.asyncio async def test_group_policy_allowlist_per_group(self): config = {"group_policy": "allowlist", "groups": {"group_abc": {"allow_from": ["yb:user_001"]}}} result = await check_group_policy("group_abc", "yb:user_001", config) assert result is True @pytest.mark.asyncio async def test_group_policy_allowlist_per_group_wildcard(self): config = {"group_policy": "allowlist", "groups": {"group_abc": {"allow_from": ["*"]}}} result = await check_group_policy("group_abc", "user_anyone", config) assert result is True @pytest.mark.asyncio async def test_group_policy_per_group_disabled(self): config = {"group_policy": "open", "groups": {"group_abc": {"enabled": False}}} result = await check_group_policy("group_abc", "user_001", config) assert result is False @pytest.mark.asyncio async def test_mention_required_disabled(self): config = {"group_require_mention": False} msg = ChannelMessage(identity=_make_identity(), content="Hello", chat_type=ChatType.GROUP) result = await check_mention_required("group_abc", msg, config) assert result is True @pytest.mark.asyncio async def test_mention_required_with_bot_mentioned(self): config = {"group_require_mention": True} msg = ChannelMessage( identity=_make_identity(), content="Hello", chat_type=ChatType.GROUP, mentions=MentionsInfo(is_bot_mentioned=True), ) result = await check_mention_required("group_abc", msg, config) assert result is True @pytest.mark.asyncio async def test_mention_required_not_mentioned(self): config = {"group_require_mention": True} msg = ChannelMessage(identity=_make_identity(), content="Hello", chat_type=ChatType.GROUP) result = await check_mention_required("group_abc", msg, config) assert result is False @pytest.mark.asyncio async def test_mention_required_by_name(self): config = {"group_require_mention": True} msg = ChannelMessage(identity=_make_identity(), content="@mybot help", chat_type=ChatType.GROUP) result = await check_mention_required("group_abc", msg, config, bot_names=["mybot"]) assert result is True @pytest.mark.asyncio async def test_mention_required_by_reply_to_bot_msg(self): config = {"group_require_mention": True} msg = ChannelMessage( identity=_make_identity(), content="Hello", chat_type=ChatType.GROUP, metadata={"reply_to_msg_id": "bot_msg_001"}, ) result = await check_mention_required("group_abc", msg, config, bot_message_ids={"bot_msg_001"}) assert result is True @pytest.mark.asyncio async def test_mention_required_default_true(self): config = {} msg = ChannelMessage(identity=_make_identity(), content="Hello", chat_type=ChatType.GROUP) result = await check_mention_required("group_abc", msg, config) assert result is False @pytest.mark.asyncio async def test_mention_required_require_mention_format(self): config = {"requireMention": False} msg = ChannelMessage(identity=_make_identity(), content="Hello", chat_type=ChatType.GROUP) result = await check_mention_required("group_abc", msg, config) assert result is True @pytest.mark.asyncio async def test_mention_required_per_group_config(self): config = {"groups": {"group_abc": {"require_mention": False}}} msg = ChannelMessage(identity=_make_identity(), content="Hello", chat_type=ChatType.GROUP) result = await check_mention_required("group_abc", msg, config) assert result is True # ==================== Session Tests ==================== class TestSessionRouting: def test_resolve_thread_key_direct(self): identity = _make_identity(channel_chat_id="user_openid_123") thread_key = resolve_thread_key(identity, chat_type="direct") assert thread_key == "yuanbao:direct:user_openid_123" def test_resolve_thread_key_group(self): identity = _make_identity(channel_chat_id="group_openid_abc") thread_key = resolve_thread_key(identity, chat_type="group") assert thread_key == "yuanbao:group:group_openid_abc" def test_resolve_thread_key_channel(self): identity = _make_identity(channel_chat_id="ch_guild_001") thread_key = resolve_thread_key(identity, chat_type="channel") assert thread_key == "yuanbao:channel:ch_guild_001" def test_resolve_thread_key_unknown_defaults_direct(self): identity = _make_identity(channel_chat_id="user_123") thread_key = resolve_thread_key(identity, chat_type="unknown") assert thread_key == "yuanbao:direct:user_123" def test_resolve_chat_type_str_direct(self): assert resolve_chat_type_str("direct") == "direct" def test_resolve_chat_type_str_group(self): assert resolve_chat_type_str("group") == "group" def test_resolve_chat_type_str_channel(self): assert resolve_chat_type_str("channel") == "guild_channel" def test_resolve_chat_type_str_unknown(self): assert resolve_chat_type_str("unknown") == "direct" def test_resolve_agent_route_direct(self): identity = _make_identity(channel_chat_id="user_123") route = resolve_agent_route(identity, chat_type="direct", default_agent_id="my_bot") assert route == "agent:my_bot:yuanbao:direct:user_123" def test_resolve_agent_route_group(self): identity = _make_identity(channel_chat_id="group_abc") route = resolve_agent_route(identity, chat_type="group", default_agent_id="default") assert route == "agent:default:yuanbao:group:group_abc" def test_resolve_agent_route_group_custom_agent(self): identity = _make_identity(channel_chat_id="group_abc") groups_config = {"group_abc": {"agent_id": "custom_agent"}} route = resolve_agent_route(identity, chat_type="group", default_agent_id="default", groups_config=groups_config) assert route == "agent:custom_agent:yuanbao:group:group_abc" def test_resolve_agent_route_channel(self): identity = _make_identity(channel_chat_id="ch_001") route = resolve_agent_route(identity, chat_type="channel", default_agent_id="default") assert route == "agent:default:yuanbao:channel:ch_001" def test_resolve_agent_route_channel_custom(self): identity = _make_identity(channel_chat_id="ch_001") channels_config = {"ch_001": {"agent_id": "channel_agent"}} route = resolve_agent_route(identity, chat_type="channel", default_agent_id="default", channels_config=channels_config) assert route == "agent:channel_agent:yuanbao:channel:ch_001" # ==================== Chunking Tests ==================== class TestChunking: def test_short_text(self): result = chunk_text("Short text", limit=20000) assert result == ["Short text"] def test_exact_limit(self): text = "A" * 5000 result = chunk_text(text, limit=6000) assert len(result) == 1 def test_multiple_chunks(self): text = "\n\n".join([f"Paragraph {i}" for i in range(200)]) result = chunk_text(text, limit=500) assert len(result) > 1 def test_single_very_long_paragraph(self): text = "A" * 10000 result = chunk_text(text, limit=1000) assert len(result) > 1 for chunk in result: assert len(chunk) <= 1000 def test_empty_text(self): result = chunk_text("", limit=1000) assert result == [""] def test_unicode_text(self): text = "\u4f60\u597d\u4e16\u754c" * 500 result = chunk_text(text, limit=300) assert len(result) > 1 assert "\u4f60\u597d\u4e16\u754c" in result[0] def test_sentence_boundary_split(self): text = "A" * 400 + "\u3002" + "B" * 400 result = chunk_text(text, limit=500) assert len(result) >= 1 def test_default_limit(self): text = "X" * 30000 result = chunk_text(text) assert len(result) > 1 # ==================== Adapter Tests ==================== class TestYuanbaoAdapter: @pytest.fixture def adapter(self): config = {"app_key": "test_key", "app_secret": "test_secret", "dm_policy": "open"} return YuanbaoAdapter(config=config) def test_channel_id(self, adapter): assert adapter.channel_id == "yuanbao" def test_channel_type(self, adapter): assert adapter.channel_type == YUANBAO def test_text_chunk_limit(self, adapter): assert adapter.text_chunk_limit == 3000 def test_supports_markdown(self, adapter): assert adapter.supports_markdown is True def test_supports_streaming(self, adapter): assert adapter.supports_streaming is True def test_streaming_modes(self, adapter): assert "off" in adapter.streaming_modes assert "block" in adapter.streaming_modes def test_max_media_size_mb(self, adapter): assert adapter.max_media_size_mb == 20 def test_initial_status(self, adapter): assert adapter.status == "disconnected" def test_webhook_path(self, adapter): assert adapter.webhook_path is None def test_credential_source_default(self, adapter): assert adapter.credential_source == "inline" def test_history_limit_default(self, adapter): assert adapter.history_limit == 100 def test_history_limit_custom(self): adapter = YuanbaoAdapter(config={"app_key": "k", "app_secret": "s", "historyLimit": 50}) assert adapter.history_limit == 50 def test_disable_block_streaming_default(self, adapter): assert adapter.disable_block_streaming is False def test_send_cache(self, adapter): assert adapter.send_cache is not None assert adapter.send_cache.size == 0 def test_markdown_hint_enabled(self, adapter): assert adapter.markdown_hint_enabled is True def test_markdown_system_hint(self, adapter): assert adapter.markdown_system_hint is not None def test_markdown_system_hint_disabled(self): adapter = YuanbaoAdapter(config={"app_key": "k", "app_secret": "s", "markdownHintEnabled": False}) assert adapter.markdown_system_hint is None def test_debug_enabled_no_ids(self, adapter): assert adapter.debug_enabled is False def test_format_outbound_text(self, adapter): resp = _make_response(content="Hello") payload = adapter.format_outbound(resp) assert payload["content"] == "Hello" assert payload["msg_type"] == "text" def test_format_outbound_group(self, adapter): resp = _make_response(content="Group msg", channel_chat_id="group_abc") resp.metadata = {"group_open_id": "group_abc"} payload = adapter.format_outbound(resp) assert payload["group_open_id"] == "group_abc" @pytest.mark.asyncio async def test_health_check_without_token(self, adapter): status = await adapter.health_check() assert status.status == "unhealthy" @pytest.mark.asyncio async def test_refresh_token_no_manager(self, adapter): result = await adapter._refresh_token_if_needed() assert result is False @pytest.mark.asyncio async def test_send_without_client(self, adapter): resp = _make_response(content="Test") result = await adapter.send(resp) assert result.success is False def test_receive_yields_nothing(self, adapter): async def _collect(): collected = [] async for msg in adapter.receive(): collected.append(msg) return collected result = asyncio.run(_collect()) assert result == [] @pytest.mark.asyncio async def test_check_security_dm_open(self, adapter): msg = ChannelMessage( identity=_make_identity(channel_chat_id="user_123"), content="Hello", chat_type=ChatType.DIRECT, ) result = await adapter._check_security(msg) assert result is True @pytest.mark.asyncio async def test_check_security_group_open(self, adapter): adapter.config["group_policy"] = "open" adapter.config["group_require_mention"] = False msg = ChannelMessage( identity=_make_identity(channel_chat_id="group_abc"), content="Hello", chat_type=ChatType.GROUP, metadata={"group_open_id": "group_abc"}, ) result = await adapter._check_security(msg) assert result is True @pytest.mark.asyncio async def test_check_security_channel(self, adapter): msg = ChannelMessage( identity=_make_identity(channel_chat_id="ch_001"), content="Hello", chat_type=ChatType.GUILD_CHANNEL, ) result = await adapter._check_security(msg) assert result is True def test_resolve_chat_context_direct(self, adapter): raw = {"open_id": "user_001", "chat_type": "direct"} chat_type, chat_id = adapter._resolve_chat_context(raw) assert chat_type == ChatType.DIRECT assert chat_id == "user_001" def test_resolve_chat_context_group(self, adapter): raw = {"open_id": "user_002", "group_open_id": "group_abc", "chat_type": "group"} chat_type, chat_id = adapter._resolve_chat_context(raw) assert chat_type == ChatType.GROUP assert chat_id == "group_abc" def test_resolve_chat_context_channel(self, adapter): raw = {"open_id": "user_003", "channel_id": "ch_001", "chat_type": "channel"} chat_type, chat_id = adapter._resolve_chat_context(raw) assert chat_type == ChatType.GUILD_CHANNEL assert chat_id == "ch_001" def test_resolve_chat_context_channel_priority(self, adapter): raw = {"open_id": "user_004", "group_open_id": "group_abc", "channel_id": "ch_001", "chat_type": "channel"} chat_type, chat_id = adapter._resolve_chat_context(raw) assert chat_type == ChatType.GUILD_CHANNEL assert chat_id == "ch_001" def test_resolve_chat_context_no_id_defaults(self, adapter): raw = {"chat_type": "direct"} chat_type, chat_id = adapter._resolve_chat_context(raw) assert chat_type == ChatType.DIRECT assert chat_id == "" def test_resolve_env_var(self): import os os.environ["TEST_YB_KEY"] = "resolved_value" result = YuanbaoAdapter._resolve_env_var("${TEST_YB_KEY}") assert result == "resolved_value" def test_resolve_env_var_not_set(self): result = YuanbaoAdapter._resolve_env_var("${NONEXISTENT_VAR_XYZ}") assert result == "${NONEXISTENT_VAR_XYZ}" def test_resolve_env_var_no_pattern(self): result = YuanbaoAdapter._resolve_env_var("plain_text") assert result == "plain_text" def test_is_allowed_domain_valid(self, adapter): assert adapter._is_allowed_domain("https://open-api.yuanbao.tencent.com/api/test") is True def test_is_allowed_domain_invalid(self, adapter): assert adapter._is_allowed_domain("https://evil.com/api/test") is False def test_is_allowed_domain_custom_api_base(self): adapter = YuanbaoAdapter(config={"app_key": "k", "app_secret": "s", "apiBase": "https://custom.api.com"}) assert adapter._is_allowed_domain("https://custom.api.com/api/test") is True def test_is_allowed_domain_invalid_url(self, adapter): assert adapter._is_allowed_domain("not a url") is False def test_bind_chat_to_account(self, adapter): adapter.bind_chat_to_account("chat_001", "acc_001") assert adapter._chat_account_map["chat_001"] == "acc_001" def test_unbind_chat(self, adapter): adapter._chat_account_map["chat_001"] = "acc_001" adapter.unbind_chat("chat_001") assert "chat_001" not in adapter._chat_account_map def test_unbind_chat_no_exist(self, adapter): adapter.unbind_chat("nonexistent") def test_get_account_status_empty(self, adapter): status = adapter.get_account_status() assert status == {} @pytest.mark.asyncio async def test_edit_message_not_supported(self, adapter): result = await adapter.edit_message("chat_1", "msg_1", "new content") assert result.success is False assert "not supported" in result.error @pytest.mark.asyncio async def test_delete_message_not_supported(self, adapter): result = await adapter.delete_message("chat_1", "msg_1") assert result.success is False @pytest.mark.asyncio async def test_pin_message_not_supported(self, adapter): result = await adapter.pin_message("chat_1", "msg_1") assert result.success is False @pytest.mark.asyncio async def test_send_poll_not_supported(self, adapter): result = await adapter.send_poll("chat_1", "Question?", ["A", "B"]) assert result.success is False @pytest.mark.asyncio async def test_send_chat_action_not_supported(self, adapter): result = await adapter.send_chat_action("chat_1", "typing") assert result.success is False # ==================== Adapter send_policies Tests ==================== class TestAdapterSendPolicies: @pytest.fixture def adapter(self): return YuanbaoAdapter(config={"app_key": "k", "app_secret": "s", "dm_policy": "open"}) def test_apply_send_policies_empty_content_fallback(self, adapter): resp = _make_response(content="", channel_chat_id="user_001") result = adapter._apply_send_policies(resp) assert result is not None assert result.content != "" def test_apply_send_policies_empty_content_no_fallback(self, adapter): adapter.config["fallbackReply"] = "" resp = _make_response(content="", channel_chat_id="user_001") result = adapter._apply_send_policies(resp) assert result is None def test_apply_send_policies_empty_content_with_attachments(self, adapter): adapter.config["fallbackReply"] = "" resp = _make_response(content="", channel_chat_id="user_001") resp.attachments = [Attachment(type="image", url="https://example.com/img.png")] result = adapter._apply_send_policies(resp) assert result is not None def test_apply_send_policies_reply_to_mode_off(self, adapter): adapter.config["replyToMode"] = "off" resp = _make_response(content="test", reply_to_message_id="msg_001") result = adapter._apply_send_policies(resp) assert result.reply_to_message_id is None def test_apply_send_policies_reply_to_mode_all(self, adapter): adapter.config["replyToMode"] = "all" resp = _make_response(content="test", reply_to_message_id="msg_001") result = adapter._apply_send_policies(resp) assert result.reply_to_message_id == "msg_001" def test_apply_send_policies_bot_message_reply_stripped(self, adapter): adapter._bot_message_ids.add("bot_msg_001") resp = _make_response(content="test", reply_to_message_id="bot_msg_001") result = adapter._apply_send_policies(resp) assert result.reply_to_message_id is None def test_apply_send_policies_attachment_size_filter(self, adapter): adapter.config["mediaMaxMb"] = 1 resp = _make_response(content="test", channel_chat_id="user_001") resp.attachments = [ Attachment(type="image", url="https://example.com/small.jpg", size_bytes=1024), Attachment(type="image", url="https://example.com/large.jpg", size_bytes=5 * 1024 * 1024), ] result = adapter._apply_send_policies(resp) assert len(result.attachments) == 1 assert result.attachments[0].url == "https://example.com/small.jpg" def test_apply_send_policies_no_attachments(self, adapter): resp = _make_response(content="test") result = adapter._apply_send_policies(resp) assert result is not None def test_check_first_reply_new(self, adapter): result = adapter._check_first_reply("new_msg_id") assert result is True assert "new_msg_id" in adapter._first_reply_db def test_check_first_reply_duplicate(self, adapter): adapter._check_first_reply("msg_dup") result = adapter._check_first_reply("msg_dup") assert result is False def test_cleanup_first_reply_db(self, adapter): now = time.time() adapter._first_reply_db["old_msg"] = now - 120 adapter._first_reply_db["new_msg"] = now adapter._cleanup_first_reply_db(now) assert "old_msg" not in adapter._first_reply_db assert "new_msg" in adapter._first_reply_db # ==================== Streaming Tests ==================== class TestStreaming: @pytest.mark.asyncio async def test_stream_blocks(self): text = "Para 1\n\nPara 2\n\nPara 3" sent_messages = [] async def mock_send(response): sent_messages.append(response.content) await send_blocks_stream("chat_001", text, mock_send, chunk_size=1) assert len(sent_messages) > 0 full_text = " ".join(sent_messages) assert "Para 1" in full_text assert "Para 2" in full_text assert "Para 3" in full_text @pytest.mark.asyncio async def test_stream_with_metadata(self): text = "Single block" sent_messages = [] async def mock_send(response): sent_messages.append(response) await send_blocks_stream("chat_001", text, mock_send, metadata={"group_open_id": "group_abc"}, chunk_size=1) assert len(sent_messages) == 1 assert sent_messages[0].metadata["group_open_id"] == "group_abc" @pytest.mark.asyncio async def test_stream_empty_text(self): sent_messages = [] async def mock_send(response): sent_messages.append(response) await send_blocks_stream("chat_001", "", mock_send) assert len(sent_messages) == 0 @pytest.mark.asyncio async def test_stream_single_paragraph(self): sent_messages = [] async def mock_send(response): sent_messages.append(response.content) await send_blocks_stream("chat_001", "Single paragraph text", mock_send, chunk_size=5000) assert len(sent_messages) == 1 @pytest.mark.asyncio async def test_stream_manager_append_and_finalize(self): sent = [] async def mock_send(response): sent.append(response.content) mgr = StreamManager(chat_id="chat_001", send_fn=mock_send, chunk_size=10) await mgr.append("Hello ") await mgr.append("World") count = await mgr.finalize() assert count >= 1 full = " ".join(sent) assert "Hello" in full @pytest.mark.asyncio async def test_stream_manager_cancel(self): sent = [] async def mock_send(response): sent.append(response.content) import time mgr = StreamManager(chat_id="chat_001", send_fn=mock_send, throttle_ms=999999) mgr._last_flush = time.monotonic() await mgr.append("Some text") await mgr.cancel() count = await mgr.finalize() assert count == 0 assert mgr._buffer == "" assert mgr._finalized is True @pytest.mark.asyncio async def test_stream_manager_disabled(self): sent = [] async def mock_send(response): sent.append(response.content) mgr = StreamManager(chat_id="chat_001", send_fn=mock_send, enabled=False) await mgr.append("text") count = await mgr.finalize() assert count == 0 @pytest.mark.asyncio async def test_lane_stream_manager(self): sent = [] async def mock_send(response): sent.append(response) mgr = LaneStreamManager(chat_id="chat_001", send_fn=mock_send) await mgr.append_to_lane("lane_a", "Hello from A") await mgr.append_to_lane("lane_b", "Hello from B") count_a = await mgr.finalize_lane("lane_a") count_b = await mgr.finalize_lane("lane_b") assert count_a >= 1 assert count_b >= 1 @pytest.mark.asyncio async def test_reasoning_stream_manager(self): sent = [] async def mock_send(response): sent.append(response) mgr = ReasoningStreamManager(chat_id="chat_001", send_fn=mock_send) await mgr.append_reasoning("Thinking...") await mgr.append_answer("The answer is 42") results = await mgr.finalize_all() assert "reasoning" in results assert "answer" in results def test_create_stream_manager_text(self): mgr = create_stream_manager("chat_1", lambda x: None, mode=StreamMode.TEXT) assert isinstance(mgr, StreamManager) def test_create_stream_manager_lane(self): mgr = create_stream_manager("chat_1", lambda x: None, mode=StreamMode.LANE) assert isinstance(mgr, LaneStreamManager) def test_create_stream_manager_reasoning(self): mgr = create_stream_manager("chat_1", lambda x: None, mode=StreamMode.REASONING) assert isinstance(mgr, ReasoningStreamManager) # ==================== Probe Tests ==================== class TestProbe: @pytest.mark.asyncio async def test_health_check_healthy(self): mock_resp = AsyncMock() mock_resp.status = 200 mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) mock_resp.__aexit__ = AsyncMock(return_value=None) mock_resp.json = AsyncMock(return_value={"bot_app_id": "bot_001"}) with patch("aiohttp.ClientSession.get", return_value=mock_resp): result = await health_check_yuanbao( "https://open-api.yuanbao.tencent.com", "test_token", ws_connected=True, ) assert result.status == "healthy" assert result.metadata["ws_connected"] is True @pytest.mark.asyncio async def test_health_check_unhealthy_auth(self): mock_resp = AsyncMock() mock_resp.status = 401 mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) mock_resp.__aexit__ = AsyncMock(return_value=None) with patch("aiohttp.ClientSession.get", return_value=mock_resp): result = await health_check_yuanbao("https://open-api.yuanbao.tencent.com", "bad_token") assert result.status == "unhealthy" assert "Token" in result.last_error @pytest.mark.asyncio async def test_health_check_exception(self): with patch("aiohttp.ClientSession.get", side_effect=Exception("Network error")): result = await health_check_yuanbao("https://open-api.yuanbao.tencent.com", "test_token") assert result.status == "unhealthy" assert "Network error" in result.last_error @pytest.mark.asyncio async def test_health_check_degraded(self): mock_resp = AsyncMock() mock_resp.status = 500 mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) mock_resp.__aexit__ = AsyncMock(return_value=None) with patch("aiohttp.ClientSession.get", return_value=mock_resp): result = await health_check_yuanbao("https://open-api.yuanbao.tencent.com", "test_token") assert result.status == "degraded" # ==================== Dispatch Tests ==================== class TestDispatch: def test_dispatch_action_values(self): assert DispatchAction.MESSAGE == "message" assert DispatchAction.COMMAND == "command" assert DispatchAction.CARD_ACTION == "card_action" assert DispatchAction.BOT_MENU == "bot_menu" assert DispatchAction.REACTION == "reaction" assert DispatchAction.MEMBER_EVENT == "member_event" assert DispatchAction.READ_RECEIPT == "read_receipt" assert DispatchAction.SYSTEM_EVENT == "system_event" assert DispatchAction.UNKNOWN == "unknown" def test_card_action_from_event(self): event = { "action_id": "act_001", "type": "card_click", "button_id": "btn_1", "value": "hello", "open_id": "user_123", "group_open_id": "group_abc", } card = CardAction.from_event(event) assert card.action_id == "act_001" assert card.action_type == "card_click" assert card.button_id == "btn_1" assert card.value == "hello" assert card.user_id == "user_123" assert card.chat_id == "group_abc" def test_card_action_from_event_button_obj(self): event = { "open_id": "user_123", "button": {"id": "btn_nested"}, } card = CardAction.from_event(event) assert card.button_id == "btn_nested" def test_bot_menu_action_from_event(self): event = { "menu_id": "menu_001", "menu_name": "Main Menu", "open_id": "user_123", "group_open_id": "group_abc", } menu = BotMenuAction.from_event(event) assert menu.menu_id == "menu_001" assert menu.menu_name == "Main Menu" assert menu.user_id == "user_123" assert menu.chat_id == "group_abc" def test_bot_menu_action_from_event_content_fallback(self): event = { "content": "Fallback Menu", "open_id": "user_123", } menu = BotMenuAction.from_event(event) assert menu.menu_name == "Fallback Menu" def test_classify_event_message(self): assert InteractiveDispatcher.classify_event({"type": "message", "content": "hello"}) == DispatchAction.MESSAGE def test_classify_event_command(self): assert InteractiveDispatcher.classify_event({"type": "message", "content": "/help"}) == DispatchAction.COMMAND def test_classify_event_edited_message(self): assert InteractiveDispatcher.classify_event({"type": "edited_message"}) == DispatchAction.MESSAGE def test_classify_event_channel_post(self): assert InteractiveDispatcher.classify_event({"type": "channel_post"}) == DispatchAction.MESSAGE def test_classify_event_reaction(self): assert InteractiveDispatcher.classify_event({"type": "reaction_added"}) == DispatchAction.REACTION def test_classify_event_bot_menu(self): assert InteractiveDispatcher.classify_event({"type": "bot_menu"}) == DispatchAction.BOT_MENU def test_classify_event_member_joined(self): assert InteractiveDispatcher.classify_event({"type": "member_joined"}) == DispatchAction.MEMBER_EVENT def test_classify_event_card_action(self): assert InteractiveDispatcher.classify_event({"type": "card_action"}) == DispatchAction.CARD_ACTION def test_classify_event_read_receipt(self): assert InteractiveDispatcher.classify_event({"type": "read_receipt"}) == DispatchAction.READ_RECEIPT def test_classify_event_typing(self): assert InteractiveDispatcher.classify_event({"type": "typing"}) == DispatchAction.SYSTEM_EVENT def test_classify_event_unknown(self): assert InteractiveDispatcher.classify_event({"type": "weird_event_type"}) == DispatchAction.UNKNOWN @pytest.mark.asyncio async def test_dispatcher_register_and_dispatch(self): dispatcher = InteractiveDispatcher() ctx = DispatchContext(action=DispatchAction.MESSAGE, raw_event={"type": "message"}) async def handler(ctx): return DispatchResult(handled=True, action=ctx.action) dispatcher.register(DispatchAction.MESSAGE, handler) result = await dispatcher.dispatch(ctx) assert result.handled is True @pytest.mark.asyncio async def test_dispatcher_fallback(self): dispatcher = InteractiveDispatcher() ctx = DispatchContext(action=DispatchAction.UNKNOWN, raw_event={"type": "unknown"}) async def fallback(ctx): return DispatchResult(handled=True, action=ctx.action, metadata={"fallback": True}) dispatcher.set_fallback(fallback) result = await dispatcher.dispatch(ctx) assert result.handled is True assert result.metadata["fallback"] is True @pytest.mark.asyncio async def test_dispatcher_no_handler_no_fallback(self): dispatcher = InteractiveDispatcher() ctx = DispatchContext(action=DispatchAction.UNKNOWN, raw_event={}) result = await dispatcher.dispatch(ctx) assert result.handled is False @pytest.mark.asyncio async def test_dispatcher_handler_error(self): dispatcher = InteractiveDispatcher() ctx = DispatchContext(action=DispatchAction.MESSAGE, raw_event={}) async def bad_handler(ctx): raise RuntimeError("test error") dispatcher.register(DispatchAction.MESSAGE, bad_handler) result = await dispatcher.dispatch(ctx) assert result.handled is False # ==================== Event Queue Tests ==================== class TestEventQueue: @pytest.mark.asyncio async def test_event_queue_start_stop(self): queue = EventQueue(max_size=100) events = [] async def handler(event): events.append(event) await queue.start(handler) assert queue.size == 0 await queue.stop() assert queue._running is False @pytest.mark.asyncio async def test_event_queue_enqueue_process(self): queue = EventQueue(max_size=100) events = [] async def handler(event): events.append(event) await queue.start(handler) await queue.enqueue({"type": "message", "content": "hello"}) await asyncio_sleep_short() await queue.stop() assert len(events) >= 1 @pytest.mark.asyncio async def test_event_queue_overflow(self): queue = EventQueue(max_size=1) await queue.start(lambda e: asyncio.sleep(0.1)) await queue.enqueue({"type": "msg1"}) await queue.enqueue({"type": "msg2"}) await queue.enqueue({"type": "msg3"}) assert queue._dropped_count > 0 @pytest.mark.asyncio async def test_event_queue_dropped_count_initial(self): queue = EventQueue() assert queue._dropped_count == 0 def asyncio_sleep_short(): return asyncio.sleep(0.01) # ==================== Outbound Queue Tests ==================== class TestOutboundQueue: @pytest.mark.asyncio async def test_immediate_strategy(self): sent = [] async def send_fn(response): sent.append(response) return DeliveryResult(success=True) queue = OutboundQueue(config={"outboundQueueStrategy": "immediate"}, send_fn=send_fn) resp = _make_response(content="Hello") result = await queue.enqueue(resp) assert result.success is True assert len(sent) == 1 @pytest.mark.asyncio async def test_merge_strategy_flush_on_max(self): sent = [] async def send_fn(response): sent.append(response) return DeliveryResult(success=True) queue = OutboundQueue( config={"outboundQueueStrategy": "merge-text", "maxChars": 50, "minChars": 2000, "idleMs": 5000}, send_fn=send_fn, ) resp = _make_response(content="A" * 60) result = await queue.enqueue(resp) assert result.success is True assert len(sent) == 1 @pytest.mark.asyncio async def test_flush_empty(self): async def send_fn(response): return DeliveryResult(success=True) queue = OutboundQueue(config={}, send_fn=send_fn) result = await queue.flush() assert result is None @pytest.mark.asyncio async def test_merge_flush_content(self): sent = [] async def send_fn(response): sent.append(response) return DeliveryResult(success=True) queue = OutboundQueue(config={"outboundQueueStrategy": "merge-text", "maxChars": 5000, "minChars": 5000, "idleMs": 5000}, send_fn=send_fn) await queue.enqueue(_make_response(content="Part 1")) await queue.enqueue(_make_response(content="Part 2")) result = await queue.flush() assert result.success is True assert len(sent) == 1 assert "Part 1" in sent[0].content assert "Part 2" in sent[0].content # ==================== Send Tests ==================== class TestSendWithRetry: @pytest.mark.asyncio async def test_send_success(self): mock_resp = AsyncMock() mock_resp.status = 200 mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) mock_resp.__aexit__ = AsyncMock(return_value=None) mock_resp.json = AsyncMock(return_value={"msg_id": "sent_001"}) mock_session = MagicMock() mock_session.post.return_value = mock_resp result = await send_with_retry( mock_session, "token", "https://api.example.com", {"content": "test"}, {} ) assert result.success is True assert result.message_id == "sent_001" @pytest.mark.asyncio async def test_send_auth_expired(self): mock_resp = AsyncMock() mock_resp.status = 401 mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) mock_resp.__aexit__ = AsyncMock(return_value=None) mock_session = MagicMock() mock_session.post.return_value = mock_resp result = await send_with_retry( mock_session, "bad_token", "https://api.example.com", {"content": "test"}, {} ) assert result.success is False assert result.auth_expired is True @pytest.mark.asyncio async def test_send_timeout(self): mock_session = MagicMock() class TimeoutContextManager: async def __aenter__(self): raise TimeoutError() async def __aexit__(self, *args): pass mock_session.post.return_value = TimeoutContextManager() result = await send_with_retry( mock_session, "token", "https://api.example.com", {"content": "test"}, {"retry": {"attempts": 1, "min_delay_ms": 1, "max_delay_ms": 10}}, ) assert result.success is False @pytest.mark.asyncio async def test_send_client_error(self): import aiohttp mock_session = MagicMock() class ClientErrorContextManager: async def __aenter__(self): raise aiohttp.ClientError("Connection failed") async def __aexit__(self, *args): pass mock_session.post.return_value = ClientErrorContextManager() result = await send_with_retry( mock_session, "token", "https://api.example.com", {"content": "test"}, {"retry": {"attempts": 1, "min_delay_ms": 1, "max_delay_ms": 10}}, ) assert result.success is False # ==================== Send Cache Tests ==================== class TestSendCache: def test_sent_message_entry_to_dict(self): entry = SentMessageEntry(msg_id="msg_001", chat_id="chat_001", content_preview="Hello", status="sent") d = entry.to_dict() assert d["msg_id"] == "msg_001" assert d["chat_id"] == "chat_001" assert d["content_preview"] == "Hello" def test_sent_message_entry_from_dict(self): data = {"msg_id": "msg_001", "chat_id": "chat_001", "content_preview": "Hi", "sent_at": 123.0, "status": "sent"} entry = SentMessageEntry.from_dict(data) assert entry.msg_id == "msg_001" assert entry.chat_id == "chat_001" def test_cache_add_and_get(self): cache = SendMessageCache() cache.add("msg_001", "chat_001", "Hello World") entry = cache.get("msg_001") assert entry is not None assert entry.msg_id == "msg_001" def test_cache_get_missing(self): cache = SendMessageCache() assert cache.get("nonexistent") is None def test_cache_size(self): cache = SendMessageCache() cache.add("msg_1", "chat_1") cache.add("msg_2", "chat_1") assert cache.size == 2 def test_cache_remove(self): cache = SendMessageCache() cache.add("msg_1", "chat_1") assert cache.remove("msg_1") is True assert cache.remove("msg_1") is False def test_cache_clear(self): cache = SendMessageCache() cache.add("msg_1", "chat_1") cache.clear() assert cache.size == 0 def test_cache_get_by_chat(self): cache = SendMessageCache() cache.add("msg_1", "chat_a") cache.add("msg_2", "chat_b") entries = cache.get_by_chat("chat_a") assert len(entries) == 1 assert entries[0].msg_id == "msg_1" def test_cache_update_status(self): cache = SendMessageCache() cache.add("msg_1", "chat_1") assert cache.update_status("msg_1", "delivered") is True assert cache.get("msg_1").status == "delivered" def test_cache_update_status_missing(self): cache = SendMessageCache() assert cache.update_status("nonexistent", "delivered") is False def test_cache_mark_deleted(self): cache = SendMessageCache() cache.add("msg_1", "chat_1") assert cache.mark_deleted("msg_1") is True assert cache.get("msg_1").status == "deleted" def test_cache_mark_edited(self): cache = SendMessageCache() cache.add("msg_1", "chat_1", "Original") assert cache.mark_edited("msg_1", "Updated") is True entry = cache.get("msg_1") assert entry.status == "edited" assert entry.content_preview == "Updated" def test_cache_get_by_status(self): cache = SendMessageCache() cache.add("msg_1", "chat_1") cache.add("msg_2", "chat_1") cache.mark_deleted("msg_1") deleted = cache.get_by_status("deleted") assert len(deleted) == 1 def test_cache_stats(self): cache = SendMessageCache() cache.add("msg_1", "chat_1") cache.mark_deleted("msg_1") cache.add("msg_2", "chat_1") stats = cache.stats assert stats.get("deleted", 0) == 1 assert stats.get("sent", 0) == 1 def test_cache_has_sent_in_chat(self): cache = SendMessageCache() cache.add("msg_1", "chat_a") assert cache.has_sent_in_chat("chat_a") is True assert cache.has_sent_in_chat("chat_b") is False def test_cache_content_preview_truncation(self): cache = SendMessageCache() cache.add("msg_1", "chat_1", "A" * 300) entry = cache.get("msg_1") assert len(entry.content_preview) <= 200 # ==================== YbAccounts Tests ==================== class TestYuanbaoAccountManager: def test_account_creation(self): acc = YuanbaoAccount(account_id="acc_1", app_key="key", app_secret="secret", name="Test") assert acc.account_id == "acc_1" assert acc.display_name == "Test" def test_account_display_name_fallback(self): acc = YuanbaoAccount(account_id="acc_1", app_key="key", app_secret="secret") assert acc.display_name == "acc_1" def test_manager_add_account(self): mgr = YuanbaoAccountManager() mgr.add_account("acc_1", "key", "secret", name="Account 1") assert mgr.account_count == 1 assert mgr.default_account_id == "acc_1" def test_manager_get_account(self): mgr = YuanbaoAccountManager() mgr.add_account("acc_1", "key", "secret") acc = mgr.get_account("acc_1") assert acc is not None assert acc.account_id == "acc_1" def test_manager_get_account_disabled(self): mgr = YuanbaoAccountManager() mgr.add_account("acc_1", "key", "secret", enabled=False) assert mgr.get_account("acc_1") is None def test_manager_get_default_account(self): mgr = YuanbaoAccountManager() mgr.add_account("acc_1", "key1", "secret1") mgr.add_account("acc_2", "key2", "secret2") assert mgr.get_default_account().account_id == "acc_1" def test_manager_remove_account(self): mgr = YuanbaoAccountManager() mgr.add_account("acc_1", "key1", "secret1") mgr.add_account("acc_2", "key2", "secret2") mgr.remove_account("acc_1") assert mgr.account_count == 1 assert mgr.default_account_id == "acc_2" def test_manager_remove_all_accounts(self): mgr = YuanbaoAccountManager() mgr.add_account("acc_1", "key1", "secret1") mgr.remove_account("acc_1") assert mgr.account_count == 0 assert mgr.default_account_id == "" def test_manager_enabled_accounts(self): mgr = YuanbaoAccountManager() mgr.add_account("acc_1", "key1", "secret1", enabled=True) mgr.add_account("acc_2", "key2", "secret2", enabled=False) assert mgr.account_count == 1 assert len(mgr.enabled_accounts) == 1 def test_load_accounts_from_config_empty(self): config = {"app_key": "key", "app_secret": "secret"} mgr = load_accounts_from_config(config) assert mgr.account_count == 1 def test_load_accounts_from_config_with_accounts(self): config = { "app_key": "key", "app_secret": "secret", "accounts": { "acc_1": {"app_key": "key1", "app_secret": "secret1", "name": "Bot 1"}, "acc_2": {"app_key": "key2", "app_secret": "secret2", "name": "Bot 2"}, }, "defaultAccount": "acc_2", } mgr = load_accounts_from_config(config) assert mgr.account_count == 2 assert mgr.default_account_id == "acc_2" # ==================== YbCommands Tests ==================== class TestYbCommands: def test_parse_command_simple(self): assert parse_command("/help") == ("help", None) def test_parse_command_with_args(self): assert parse_command("/help status") == ("help", "status") def test_parse_command_not_a_command(self): assert parse_command("Hello") == (None, None) def test_parse_command_empty(self): assert parse_command("") == (None, None) def test_parse_command_whitespace(self): assert parse_command(" /status ") == ("status", None) def test_command_definitions_structure(self): names = [cmd["name"] for cmd in COMMAND_DEFINITIONS] assert "help" in names assert "status" in names assert "new" in names @pytest.mark.asyncio async def test_handle_help_list(self): ctx = NativeCommandContext(user_id="u1", chat_id="c1", chat_type="direct") result = await handle_command("help", None, ctx) assert result.action == "help_list" assert result.response_text is not None @pytest.mark.asyncio async def test_handle_help_detail(self): ctx = NativeCommandContext(user_id="u1", chat_id="c1", chat_type="direct") result = await handle_command("help", "status", ctx) assert result.action == "help_detail" assert "status" in result.response_text @pytest.mark.asyncio async def test_handle_help_not_found(self): ctx = NativeCommandContext(user_id="u1", chat_id="c1", chat_type="direct") result = await handle_command("help", "nonexistent", ctx) assert result.action == "help_not_found" @pytest.mark.asyncio async def test_handle_status(self): ctx = NativeCommandContext(user_id="u1", chat_id="c1", chat_type="direct", adapter_status="connected") result = await handle_command("status", None, ctx) assert result.action == "status_report" assert "connected" in result.response_text @pytest.mark.asyncio async def test_handle_new(self): ctx = NativeCommandContext(user_id="u1", chat_id="c1", chat_type="direct") result = await handle_command("new", None, ctx) assert result.action == "new_session" @pytest.mark.asyncio async def test_handle_stop(self): ctx = NativeCommandContext(user_id="u1", chat_id="c1", chat_type="direct") result = await handle_command("stop", None, ctx) assert result.action == "stop_task" assert result.metadata["request_stop"] is True @pytest.mark.asyncio async def test_handle_restart(self): ctx = NativeCommandContext(user_id="u1", chat_id="c1", chat_type="direct") result = await handle_command("restart", None, ctx) assert result.action == "restart_session" @pytest.mark.asyncio async def test_handle_compact(self): ctx = NativeCommandContext(user_id="u1", chat_id="c1", chat_type="direct") result = await handle_command("compact", None, ctx) assert result.action == "compact_history" @pytest.mark.asyncio async def test_handle_unknown_command(self): ctx = NativeCommandContext(user_id="u1", chat_id="c1", chat_type="direct") result = await handle_command("foobar", None, ctx) assert result.action == "unknown" @pytest.mark.asyncio async def test_sync_commands_menu_success(self): mock_resp = AsyncMock() mock_resp.status = 200 mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) mock_resp.__aexit__ = AsyncMock(return_value=None) mock_client = MagicMock() mock_client.post.return_value = mock_resp result = await sync_commands_menu("https://api.example.com", "token", mock_client) assert result is True # ==================== Config Mapper Tests ==================== class TestConfigMapper: def test_normalize_config_basic(self): config = {"app_key": "key", "app_secret": "secret"} result = normalize_config(config) assert result["app_key"] == "key" def test_normalize_config_flatten_dm(self): config = {"dm": {"policy": "allowlist", "allowFrom": ["user_1"]}} result = normalize_config(config) assert result["dm_policy"] == "allowlist" assert result["allow_from"] == ["user_1"] def test_normalize_config_flat_dm_takes_priority(self): config = {"dm_policy": "open", "dm": {"policy": "disabled"}} result = normalize_config(config) assert result["dm_policy"] == "open" def test_normalize_config_flatten_accounts(self): config = {"accounts": {"acc_1": {"appKey": "key1"}}} result = normalize_config(config) assert result["accounts"]["acc_1"]["app_key"] == "key1" def test_normalize_config_flatten_groups(self): config = {"groups": {"g1": {"requireMention": True}}} result = normalize_config(config) assert result["groups"]["g1"]["require_mention"] is True def test_to_openclaw_key(self): assert to_openclaw_key("app_key") == "appKey" assert to_openclaw_key("unknown_key") == "unknown_key" # ==================== Proto Codec Tests ==================== class TestProtoCodec: def test_is_protobuf(self): assert ProtoCodec.is_protobuf(bytes([0xFE, 0x01, 0x01, 0x00, 0x00])) is True assert ProtoCodec.is_protobuf(b"\x00\x00") is False def test_is_protobuf_message_func(self): assert is_protobuf_message(bytes([0xFE, 0x01, 0x01, 0x00, 0x00])) is True assert is_protobuf_message(b"hello") is False def test_encode_decode_roundtrip(self): payload = {"type": "message", "content": "hello"} frame = ProtoCodec.encode_biz(payload) decoded = ProtoCodec.decode(frame) assert decoded is not None assert decoded["payload"]["content"] == "hello" assert decoded["msg_type"] == "biz" def test_encode_conn(self): frame = ProtoCodec.encode_conn({"type": "connect"}) decoded = ProtoCodec.decode(frame) assert decoded["msg_type"] == "conn" def test_encode_heartbeat(self): frame = ProtoCodec.encode_heartbeat() assert len(frame) == 5 assert frame[0] == 0xFE assert frame[1] == ProtoCodec.VERSION assert frame[2] == ProtoCodec.MSG_TYPE_HEARTBEAT assert frame[3:5] == b"\x00\x00" def test_encode_auth(self): frame = ProtoCodec.encode_auth("test_token") decoded = ProtoCodec.decode(frame) assert decoded["payload"]["token"] == "test_token" assert decoded["msg_type"] == "auth" def test_decode_invalid_length(self): assert ProtoCodec.decode(b"\xFE") is None def test_decode_wrong_magic(self): assert ProtoCodec.decode(b"\x00\x01\x01\x00\x00") is None def test_decode_malformed(self): assert ProtoCodec.decode(b"\xFE\x01\x01\xFF\xFF") is None def test_body_too_large(self): with pytest.raises(ValueError, match="Body too large"): ProtoCodec.encode_biz({"data": "A" * 70000}) def test_is_protobuf_empty(self): assert ProtoCodec.is_protobuf(b"") is False # ==================== Template Tests ==================== class TestTemplate: def test_button_to_dict(self): btn = TemplateButton(text="Click me", action_type="url", value="https://example.com") d = btn.to_dict() assert d["text"] == "Click me" assert d["type"] == "url" assert d["value"] == "https://example.com" def test_selector_option_to_dict(self): opt = SelectorOption(label="Option 1", value="opt1", description="First option") d = opt.to_dict() assert d["label"] == "Option 1" assert d["description"] == "First option" def test_selector_option_to_dict_no_description(self): opt = SelectorOption(label="Option 1", value="opt1") d = opt.to_dict() assert "description" not in d def test_action_selector_to_dict(self): sel = ActionSelector(placeholder="Choose", options=[SelectorOption(label="A", value="a")], selector_id="sel_1") d = sel.to_dict() assert d["type"] == "action_select" assert d["placeholder"] == "Choose" assert len(d["options"]) == 1 def test_template_card_to_dict(self): card = TemplateCard(title="Card Title", content="Card Content", image_url="https://img.com/pic.jpg") d = card.to_dict() assert d["title"] == "Card Title" assert d["image_url"] == "https://img.com/pic.jpg" def test_template_card_to_dict_with_buttons(self): card = TemplateCard(title="Card", buttons=[TemplateButton(text="OK")]) d = card.to_dict() assert len(d["buttons"]) == 1 def test_template_message_builder(self): builder = TemplateMessageBuilder() builder.set_content("Message content") builder.add_button("Click", "url", "https://example.com") assert builder.build_content() == "Message content" metadata = builder.build_metadata() assert len(metadata["buttons"]) == 1 def test_template_message_builder_card(self): builder = TemplateMessageBuilder() card = TemplateCard(title="Card") builder.set_card(card) metadata = builder.build_metadata() assert metadata["card"] is not None def test_template_message_builder_selector(self): builder = TemplateMessageBuilder() builder.add_selector(placeholder="Select...", options=[SelectorOption(label="L", value="v")]) metadata = builder.build_metadata() assert len(metadata["selectors"]) == 1 def test_template_message_builder_reset(self): builder = TemplateMessageBuilder() builder.set_content("test") builder.reset() assert builder.build_content() == "" assert builder.build_metadata() == {} # ==================== Doc Gen Tests ==================== class TestDocGen: def test_generate_channel_docs(self): caps = ChannelCapabilities(chat_types=["direct", "group"], delivery_mode="direct") meta = ChannelMeta(id="yuanbao", label="Test", selection_label="Test", blurb="Test channel", order=1) result = generate_channel_docs("yuanbao", caps, meta) assert "Test" in result assert "yuanbao" in result assert "## \u80fd\u529b\u77e9\u9635" in result assert "## \u914d\u7f6e\u9879\u53c2\u8003" in result def test_config_keys_have_required_fields(self): assert "app_key" in YUANBAO_CONFIG_KEYS assert "app_secret" in YUANBAO_CONFIG_KEYS assert "dm_policy" in YUANBAO_CONFIG_KEYS # ==================== Security Audit Tests ==================== class TestSecurityAudit: def test_log_dm_policy_blocked(self): SecurityAuditLogger.log_dm_policy_blocked("user_1", "test_reason", "allowlist") def test_log_group_access_blocked(self): SecurityAuditLogger.log_group_access_blocked("group_1", "user_1", "test") def test_log_mention_required_blocked(self): SecurityAuditLogger.log_mention_required_blocked("group_1", "user_1") def test_log_unauthorized_access(self): SecurityAuditLogger.log_unauthorized_access("user_1", "resource_1") def test_log_rate_limit_exceeded(self): SecurityAuditLogger.log_rate_limit_exceeded("user_1", "api") def test_log_circuit_breaker_open(self): SecurityAuditLogger.log_circuit_breaker_open("user_1", "too many failures") def test_log_auth_failure(self): SecurityAuditLogger.log_auth_failure("acc_1", "invalid token") def test_set_output_format(self): SecurityAuditLogger.set_output_format("json") SecurityAuditLogger.log_dm_policy_blocked("user_1", "test", "open") SecurityAuditLogger.set_output_format("text")