本提交新增了全渠道SDK核心模块: 1. 异步锁、目标解析、动作调度等基础工具 2. 消息动作注册与统一调度系统 3. 测试套件与契约测试框架 4. 完整的目标解析流水线与工具函数 5. 资源依赖注入与生命周期管理
170 lines
5.6 KiB
Python
170 lines
5.6 KiB
Python
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
from yuxi.channel.capabilities import ChannelCapabilities
|
|
from yuxi.channel.protocols import (
|
|
CapabilitiesProtocol,
|
|
ConfigProtocol,
|
|
MetaProtocol,
|
|
OutboundProtocol,
|
|
SecurityProtocol,
|
|
StatusProtocol,
|
|
)
|
|
|
|
|
|
class ContractTestBase:
|
|
plugin: MetaProtocol
|
|
plugin_capabilities: ChannelCapabilities | None = None
|
|
|
|
async def setup_method(self) -> None:
|
|
if hasattr(self.plugin, "capabilities"):
|
|
self.plugin_capabilities = self.plugin.capabilities
|
|
|
|
async def test_meta_protocol(self) -> None:
|
|
assert isinstance(self.plugin.id, str) and self.plugin.id, "plugin.id must be a non-empty string"
|
|
assert isinstance(self.plugin.name, str) and self.plugin.name, "plugin.name must be a non-empty string"
|
|
if hasattr(self.plugin, "label") and self.plugin.label is not None:
|
|
assert isinstance(self.plugin.label, str), "plugin.label must be a string"
|
|
|
|
async def test_capabilities(self) -> None:
|
|
if not isinstance(self.plugin, CapabilitiesProtocol):
|
|
return
|
|
caps = self.plugin.capabilities
|
|
assert isinstance(caps, ChannelCapabilities), "capabilities must be ChannelCapabilities"
|
|
for ct in caps.chat_types:
|
|
assert ct in ("direct", "group"), f"Unknown chat_type: {ct}"
|
|
for mt in caps.message_types:
|
|
assert mt in ("text", "image", "video", "audio", "file", "location"), f"Unknown message_type: {mt}"
|
|
if caps.streaming_mode is not None and caps.streaming:
|
|
valid_modes = ("card_kit", "block", "c2c_stream_api", "raw")
|
|
assert caps.streaming_mode in valid_modes, f"Unknown streaming_mode: {caps.streaming_mode}"
|
|
if caps.block_streaming:
|
|
assert caps.block_streaming_chunk_min_chars <= caps.block_streaming_chunk_max_chars, \
|
|
"block_streaming_chunk_min_chars must be <= block_streaming_chunk_max_chars"
|
|
|
|
async def test_config_protocol(self) -> None:
|
|
if not isinstance(self.plugin, ConfigProtocol):
|
|
return
|
|
default_config = self.plugin.get_default_config()
|
|
assert isinstance(default_config, dict), "get_default_config must return a dict"
|
|
|
|
async def test_all_contracts(self) -> None:
|
|
await self.setup_method()
|
|
await self.test_meta_protocol()
|
|
await self.test_capabilities()
|
|
await self.test_config_protocol()
|
|
|
|
|
|
class MetaProtocolContract(ABC):
|
|
|
|
@abstractmethod
|
|
def make_plugin(self):
|
|
...
|
|
|
|
async def test_plugin_has_id(self):
|
|
plugin = self.make_plugin()
|
|
assert plugin.id, "plugin must have a non-empty id"
|
|
|
|
async def test_plugin_has_name(self):
|
|
plugin = self.make_plugin()
|
|
assert plugin.name, "plugin must have a non-empty name"
|
|
|
|
async def test_capabilities_chat_types_non_empty(self):
|
|
plugin = self.make_plugin()
|
|
caps = plugin.capabilities
|
|
assert isinstance(caps, ChannelCapabilities)
|
|
assert caps.chat_types, "capabilities must declare at least one chat_type"
|
|
|
|
|
|
class ConfigProtocolContract(ABC):
|
|
|
|
@abstractmethod
|
|
def make_plugin(self):
|
|
...
|
|
|
|
async def test_list_account_ids_returns_list(self):
|
|
plugin = self.make_plugin()
|
|
if not isinstance(plugin, ConfigProtocol):
|
|
return
|
|
ids = plugin.list_account_ids({})
|
|
assert isinstance(ids, list), "list_account_ids must return a list"
|
|
|
|
async def test_resolve_account_returns_dict(self):
|
|
plugin = self.make_plugin()
|
|
if not isinstance(plugin, ConfigProtocol):
|
|
return
|
|
result = await plugin.resolve_account("test_account")
|
|
assert isinstance(result, dict)
|
|
|
|
async def test_is_configured_returns_bool(self):
|
|
plugin = self.make_plugin()
|
|
if not isinstance(plugin, ConfigProtocol):
|
|
return
|
|
result = plugin.is_configured({"id": "test", "enabled": True})
|
|
assert isinstance(result, bool)
|
|
|
|
|
|
class OutboundPayloadContract(ABC):
|
|
|
|
@abstractmethod
|
|
def make_plugin(self):
|
|
...
|
|
|
|
async def test_send_text_accepts_target_content(self):
|
|
plugin = self.make_plugin()
|
|
if not isinstance(plugin, OutboundProtocol):
|
|
return
|
|
await plugin.send_text("target_001", "contract test message")
|
|
|
|
async def test_send_media_accepts_media_url(self):
|
|
plugin = self.make_plugin()
|
|
if not isinstance(plugin, OutboundProtocol):
|
|
return
|
|
await plugin.send_media("target_001", "https://example.com/a.jpg", "image")
|
|
|
|
|
|
class SecurityProtocolContract(ABC):
|
|
|
|
@abstractmethod
|
|
def make_plugin(self):
|
|
...
|
|
|
|
async def test_check_allowlist_returns_bool(self):
|
|
plugin = self.make_plugin()
|
|
if not isinstance(plugin, SecurityProtocol):
|
|
return
|
|
result = await plugin.check_allowlist("test_user", "direct")
|
|
assert isinstance(result, bool), "check_allowlist must return bool"
|
|
|
|
|
|
class StatusProtocolContract(ABC):
|
|
|
|
@abstractmethod
|
|
def make_plugin(self):
|
|
...
|
|
|
|
async def test_probe_returns_bool(self):
|
|
plugin = self.make_plugin()
|
|
if not isinstance(plugin, StatusProtocol):
|
|
return
|
|
result = await plugin.probe()
|
|
assert isinstance(result, bool)
|
|
|
|
async def test_build_summary_returns_dict(self):
|
|
plugin = self.make_plugin()
|
|
if not isinstance(plugin, StatusProtocol):
|
|
return
|
|
result = plugin.build_summary(None)
|
|
assert isinstance(result, dict)
|
|
|
|
|
|
class ChannelContractTest(
|
|
MetaProtocolContract,
|
|
ConfigProtocolContract,
|
|
OutboundPayloadContract,
|
|
SecurityProtocolContract,
|
|
StatusProtocolContract,
|
|
ABC,
|
|
):
|
|
pass |