ForcePilot/backend/test/unit/channels/test_yuanbao_adapter.py

2214 lines
82 KiB
Python
Raw Normal View History

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