新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块 同时在测试配置中添加了测试用的OpenAI API密钥环境变量
265 lines
8.7 KiB
Python
265 lines
8.7 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from yuxi.channels.base import BaseChannelAdapter
|
|
from yuxi.channels.capabilities import (
|
|
CAPS_FULL_FEATURED,
|
|
CAPS_GROUP_CHAT_ONLY,
|
|
CAPS_SIMPLE_TEXT,
|
|
ChannelCapabilities,
|
|
)
|
|
from yuxi.channels.meta import ChannelMeta
|
|
from yuxi.channels.models import (
|
|
ChannelMessage,
|
|
ChannelResponse,
|
|
ChannelType,
|
|
DeliveryResult,
|
|
HealthStatus,
|
|
)
|
|
|
|
|
|
class _MinimalAdapter(BaseChannelAdapter):
|
|
channel_id = "minimal"
|
|
channel_type = ChannelType.WEBCHAT
|
|
|
|
async def connect(self) -> None:
|
|
pass
|
|
|
|
async def disconnect(self) -> None:
|
|
pass
|
|
|
|
async def send(self, response: ChannelResponse) -> DeliveryResult:
|
|
return DeliveryResult(success=True)
|
|
|
|
def normalize_inbound(self, raw) -> ChannelMessage:
|
|
raise NotImplementedError
|
|
|
|
def format_outbound(self, response: ChannelResponse):
|
|
return {"content": response.content}
|
|
|
|
async def health_check(self) -> HealthStatus:
|
|
return HealthStatus(status="healthy")
|
|
|
|
|
|
class TestChannelCapabilitiesDefaults:
|
|
def test_default_chat_types(self):
|
|
caps = ChannelCapabilities()
|
|
assert caps.chat_types == ["direct"]
|
|
|
|
def test_all_flags_false_by_default(self):
|
|
caps = ChannelCapabilities()
|
|
assert caps.polls is False
|
|
assert caps.reactions is False
|
|
assert caps.edit is False
|
|
assert caps.unsend is False
|
|
assert caps.reply is False
|
|
assert caps.effects is False
|
|
assert caps.group_management is False
|
|
assert caps.threads is False
|
|
assert caps.media is False
|
|
assert caps.tts is False
|
|
assert caps.native_commands is False
|
|
assert caps.block_streaming is False
|
|
assert caps.pin is False
|
|
assert caps.unpin is False
|
|
assert caps.list_pins is False
|
|
assert caps.send_ephemeral is False
|
|
|
|
def test_default_text_fields(self):
|
|
caps = ChannelCapabilities()
|
|
assert caps.text_chunk_limit == 4096
|
|
assert caps.supports_markdown is False
|
|
assert caps.supports_streaming is False
|
|
assert caps.streaming_modes == ["off"]
|
|
|
|
def test_default_max_media_size(self):
|
|
caps = ChannelCapabilities()
|
|
assert caps.max_media_size_mb == 100
|
|
|
|
|
|
class TestChannelCapabilitiesPresets:
|
|
def test_caps_simple_text(self):
|
|
caps = CAPS_SIMPLE_TEXT
|
|
assert caps.chat_types == ["direct"]
|
|
assert caps.supports_markdown is False
|
|
assert caps.supports_streaming is False
|
|
assert caps.streaming_modes == ["off"]
|
|
|
|
def test_caps_full_featured(self):
|
|
caps = CAPS_FULL_FEATURED
|
|
assert caps.chat_types == ["direct", "group", "thread"]
|
|
assert caps.polls is True
|
|
assert caps.reactions is True
|
|
assert caps.edit is True
|
|
assert caps.unsend is True
|
|
assert caps.reply is True
|
|
assert caps.threads is True
|
|
assert caps.media is True
|
|
assert caps.supports_markdown is True
|
|
assert caps.supports_streaming is True
|
|
assert caps.streaming_modes == ["chunked", "live"]
|
|
|
|
def test_caps_group_chat_only(self):
|
|
caps = CAPS_GROUP_CHAT_ONLY
|
|
assert caps.chat_types == ["direct", "group"]
|
|
assert caps.reply is True
|
|
assert caps.media is True
|
|
assert caps.group_management is True
|
|
assert caps.supports_markdown is True
|
|
assert caps.supports_streaming is True
|
|
assert caps.streaming_modes == ["block"]
|
|
|
|
def test_presets_are_independent(self):
|
|
simple = CAPS_SIMPLE_TEXT.model_dump()
|
|
full = CAPS_FULL_FEATURED.model_dump()
|
|
assert simple != full
|
|
|
|
|
|
class TestChannelCapabilitiesModelDump:
|
|
def test_model_dump_includes_all_fields(self):
|
|
caps = ChannelCapabilities()
|
|
dumped = caps.model_dump()
|
|
expected_fields = {
|
|
"chat_types", "polls", "reactions", "edit", "unsend",
|
|
"reply", "effects", "group_management", "threads",
|
|
"media", "max_media_size_mb", "tts", "native_commands",
|
|
"block_streaming", "pin", "unpin", "list_pins",
|
|
"send_ephemeral", "text_chunk_limit", "supports_markdown",
|
|
"supports_streaming", "streaming_modes",
|
|
}
|
|
assert set(dumped.keys()) == expected_fields
|
|
|
|
def test_model_dump_serializable(self):
|
|
import json
|
|
|
|
caps = CAPS_FULL_FEATURED
|
|
dumped = caps.model_dump()
|
|
json.dumps(dumped)
|
|
|
|
def test_model_validate_roundtrip(self):
|
|
original = CAPS_FULL_FEATURED
|
|
data = original.model_dump()
|
|
restored = ChannelCapabilities(**data)
|
|
assert restored == original
|
|
|
|
|
|
class TestChannelMeta:
|
|
def test_default_values(self):
|
|
meta = ChannelMeta(id="test", label="Test")
|
|
assert meta.id == "test"
|
|
assert meta.label == "Test"
|
|
assert meta.selection_label == ""
|
|
assert meta.docs_path == ""
|
|
assert meta.blurb == ""
|
|
assert meta.order == 100
|
|
assert meta.aliases == []
|
|
assert meta.system_image == ""
|
|
assert meta.markdown_capable is False
|
|
assert meta.exposure == "public"
|
|
assert meta.show_configured is True
|
|
assert meta.show_in_setup is True
|
|
assert meta.quickstart_allow_from == []
|
|
assert meta.force_account_binding is False
|
|
|
|
def test_full_meta_example(self):
|
|
meta = ChannelMeta(
|
|
id="telegram",
|
|
label="Telegram",
|
|
selection_label="Telegram Bot",
|
|
docs_path="docs/channels/telegram",
|
|
docs_label="Telegram 文档",
|
|
blurb="Telegram 渠道适配器",
|
|
order=5,
|
|
aliases=["tg"],
|
|
system_image="telegram",
|
|
markdown_capable=True,
|
|
exposure="public",
|
|
)
|
|
assert meta.id == "telegram"
|
|
assert meta.aliases == ["tg"]
|
|
assert meta.order == 5
|
|
assert meta.markdown_capable is True
|
|
|
|
def test_model_dump_serializable(self):
|
|
import json
|
|
|
|
meta = ChannelMeta(id="test", label="Test")
|
|
json.dumps(meta.model_dump())
|
|
|
|
|
|
class TestBaseChannelAdapterIntegration:
|
|
def test_default_capabilities(self):
|
|
assert _MinimalAdapter.capabilities == CAPS_SIMPLE_TEXT
|
|
|
|
def test_default_meta(self):
|
|
assert _MinimalAdapter.meta == ChannelMeta(id="", label="")
|
|
|
|
def test_override_capabilities(self):
|
|
class CustomAdapter(_MinimalAdapter):
|
|
channel_id = "custom"
|
|
capabilities = CAPS_FULL_FEATURED
|
|
|
|
assert CustomAdapter.capabilities == CAPS_FULL_FEATURED
|
|
assert CustomAdapter.capabilities.polls is True
|
|
assert CustomAdapter.capabilities.streaming_modes == ["chunked", "live"]
|
|
|
|
def test_override_meta(self):
|
|
class CustomAdapter(_MinimalAdapter):
|
|
channel_id = "custom"
|
|
meta = ChannelMeta(id="custom", label="Custom", order=3)
|
|
|
|
assert CustomAdapter.meta.id == "custom"
|
|
assert CustomAdapter.meta.label == "Custom"
|
|
assert CustomAdapter.meta.order == 3
|
|
|
|
def test_custom_capabilities_example(self):
|
|
class WeChatLikeAdapter(_MinimalAdapter):
|
|
channel_id = "wechat-like"
|
|
capabilities = ChannelCapabilities(
|
|
chat_types=["direct", "group"],
|
|
reply=True,
|
|
group_management=True,
|
|
media=True,
|
|
block_streaming=True,
|
|
text_chunk_limit=2048,
|
|
max_media_size_mb=20,
|
|
supports_markdown=False,
|
|
supports_streaming=False,
|
|
)
|
|
|
|
caps = WeChatLikeAdapter.capabilities
|
|
assert caps.chat_types == ["direct", "group"]
|
|
assert caps.text_chunk_limit == 2048
|
|
assert caps.max_media_size_mb == 20
|
|
assert caps.block_streaming is True
|
|
assert caps.supports_markdown is False
|
|
|
|
def test_capabilities_model_dump_integration(self):
|
|
class CustomAdapter(_MinimalAdapter):
|
|
channel_id = "custom"
|
|
capabilities = CAPS_FULL_FEATURED
|
|
|
|
dumped = CustomAdapter.capabilities.model_dump()
|
|
assert dumped["streaming_modes"] == ["chunked", "live"]
|
|
assert dumped["chat_types"] == ["direct", "group", "thread"]
|
|
|
|
def test_hasattr_compatibility(self):
|
|
assert hasattr(_MinimalAdapter, "capabilities")
|
|
assert hasattr(_MinimalAdapter, "meta")
|
|
|
|
|
|
class TestManagerCompatibility:
|
|
def test_old_adapter_has_classvar_fallback(self):
|
|
import types
|
|
|
|
OldStyle = types.new_class(
|
|
"OldStyle",
|
|
bases=(BaseChannelAdapter,),
|
|
exec_body=lambda ns: ns.update({
|
|
"channel_id": "old",
|
|
"channel_type": ChannelType.WEBCHAT,
|
|
}),
|
|
)
|
|
assert hasattr(OldStyle, "capabilities")
|
|
assert OldStyle.text_chunk_limit == 4096 |