ForcePilot/backend/package/yuxi/channel/sdk/testing/contracts.py
Kris b438af3ba8 feat(channel-sdk): 新增完整的渠道SDK工具链
本提交新增了全渠道SDK核心模块:
1.  异步锁、目标解析、动作调度等基础工具
2.  消息动作注册与统一调度系统
3.  测试套件与契约测试框架
4.  完整的目标解析流水线与工具函数
5.  资源依赖注入与生命周期管理
2026-05-21 10:29:12 +08:00

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