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