ForcePilot/backend/test/unit/channels/test_yuanbao_adapter.py
Kris 69fe97a90d test: 批量修复并新增单元测试用例
1. 移除Telegram格式化测试中未使用的导入项
2. 修复Teams测试用例,添加monkeypatch参数并配置通配符开关
3. 更新钉钉适配器测试,替换弃用的流属性检查
4. 修正Twitch规范化测试,更新ROOMSTATE测试逻辑
5. 重构会话映射测试,完善数据库执行结果模拟
6. 格式化Slack块构建测试的长参数调用
7. 修复LINE适配器测试,更新能力断言和异步锁使用
8. 修正Slack会话解析测试,修复聊天类型判断错误
9. 更新能力测试,补充缺失的字段检查
10. 修复Matrix适配器测试,修正位置参数和配置校验逻辑
11. 为飞书分析模块测试添加跳过标记
12. 新增微信能力、限流、链接格式、会话路由等模块的单元测试
13. 修复Twitch适配器导入路径和测试断言
14. 新增Discord Webhook、Nextcloud Talk、Signal多账户等模块的单元测试
15. 修复Manager阶段测试的导入路径
16. 新增iMessage异常和命令处理的单元测试
17. 新增Nostr健康检查和相关模块的单元测试
18. 新增Signal守护进程和SSE重连相关测试
2026-05-13 16:43:01 +08:00

2214 lines
82 KiB
Python

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")