from __future__ import annotations from unittest.mock import MagicMock, patch import pytest from yuxi.channel.plugins.protocol import ChannelMeta, DeliveryCapabilities, InboundMedia, OutboundMessage from yuxi.channel.plugins.registry import ChannelRegistry, get_registry from yuxi.channel.ports import MetaPort, OutboundPort @pytest.fixture def sample_plugin(): plugin = MagicMock() plugin.get_meta.return_value = ChannelMeta( channel_type="test", display_name="Test", aliases=["t", "tst"], ) return plugin @pytest.fixture def aliased_plugin(): plugin = MagicMock() plugin.get_meta.return_value = ChannelMeta( channel_type="other", display_name="Other", aliases=["t"], ) return plugin class TestChannelRegistryRegister: def test_register_stores_plugin_by_channel_type(self, sample_plugin): registry = ChannelRegistry() registry.register(sample_plugin) assert registry.get_plugin("test") is sample_plugin def test_register_stores_plugin_by_aliases(self, sample_plugin): registry = ChannelRegistry() registry.register(sample_plugin) assert registry.get_plugin("t") is sample_plugin assert registry.get_plugin("tst") is sample_plugin def test_register_primary_stores_only_channel_type(self, sample_plugin): registry = ChannelRegistry() registry.register_primary(sample_plugin) assert registry.get_plugin("test") is sample_plugin assert registry.get_plugin("t") is None def test_register_overwrites_same_plugin_without_warning(self, sample_plugin): registry = ChannelRegistry() with patch("yuxi.channel.plugins.registry.logger") as mock_logger: registry.register(sample_plugin) registry.register(sample_plugin) mock_logger.warning.assert_not_called() def test_register_overwrites_different_plugin_logs_warning(self, sample_plugin): registry = ChannelRegistry() other_plugin = MagicMock() other_plugin.get_meta.return_value = ChannelMeta(channel_type="test", display_name="Other") with patch("yuxi.channel.plugins.registry.logger") as mock_logger: registry.register(sample_plugin) registry.register(other_plugin) mock_logger.warning.assert_called_once() def test_register_alias_conflict_logs_warning_and_skips(self, sample_plugin, aliased_plugin): registry = ChannelRegistry() with patch("yuxi.channel.plugins.registry.logger") as mock_logger: registry.register(sample_plugin) registry.register(aliased_plugin) assert registry.get_plugin("t") is sample_plugin assert registry.get_plugin("other") is aliased_plugin assert mock_logger.warning.call_count == 1 class TestChannelRegistryUnregister: def test_unregister_removes_channel_type_and_aliases(self, sample_plugin): registry = ChannelRegistry() registry.register(sample_plugin) registry.unregister("test") assert registry.get_plugin("test") is None assert registry.get_plugin("t") is None assert registry.get_plugin("tst") is None def test_unregister_unknown_channel_type_is_safe(self): registry = ChannelRegistry() registry.unregister("unknown") assert registry.get_plugin("unknown") is None def test_unregister_only_removes_aliases_for_primary(self, sample_plugin, aliased_plugin): registry = ChannelRegistry() registry.register(sample_plugin) # Register alias from another plugin under a different alias aliased_plugin.get_meta.return_value.aliases = ["other-alias"] registry.register(aliased_plugin) registry.unregister("other") assert registry.get_plugin("test") is sample_plugin assert registry.get_plugin("t") is sample_plugin class TestChannelRegistryListPlugins: def test_list_plugins_dedupes_by_identity(self, sample_plugin): registry = ChannelRegistry() registry.register(sample_plugin) metas = registry.list_plugins() assert len(metas) == 1 assert metas[0].channel_type == "test" def test_list_plugins_includes_only_unique_plugins(self, sample_plugin): registry = ChannelRegistry() registry.register(sample_plugin) metas = registry.list_plugins() assert len(metas) == 1 class TestGlobalRegistry: def test_get_registry_returns_singleton(self): with patch("yuxi.channel.plugins.registry._GLOBAL_REGISTRY", None): r1 = get_registry() r2 = get_registry() assert r1 is r2 def test_get_registry_creates_new_instance_when_none(self): with patch("yuxi.channel.plugins.registry._GLOBAL_REGISTRY", None): registry = get_registry() assert isinstance(registry, ChannelRegistry) class TestPortBasedRegistryQueries: def _make_partial_plugin(self, channel_type: str = "partial"): class PartialPlugin(MetaPort, OutboundPort): def __init__(self) -> None: self._meta = ChannelMeta(channel_type=channel_type, display_name="Partial") def get_meta(self) -> ChannelMeta: return self._meta async def format_outbound(self, message: OutboundMessage, config: dict | None = None) -> dict: return {} async def send_message( self, channel_session_id: str, payload: dict, config: dict | None = None ) -> str | None: return None def chunk_text(self, text: str, limit: int) -> list[str]: return [] def get_delivery_capabilities(self) -> DeliveryCapabilities: return DeliveryCapabilities() async def enrich_outbound( self, payload: dict, message: OutboundMessage, config: dict | None = None ) -> dict: return payload async def upload_media(self, media: InboundMedia | dict, config: dict | None = None) -> dict | None: return None def supports_batch_send(self, config: dict | None = None) -> bool: return False async def send_batch( self, channel_session_id: str, payloads: list[dict], config: dict | None = None ) -> list[str | None]: return [] async def send_typing_indicator(self, channel_session_id: str, config: dict | None = None) -> None: return None def supports_content_type(self, content_type: str, config: dict | None = None) -> bool: return False return PartialPlugin() def test_register_partial_plugin_succeeds(self): registry = ChannelRegistry() plugin = self._make_partial_plugin() registry.register(plugin) assert registry.get_plugin("partial") is plugin def test_get_plugins_by_port_filters_by_outbound(self): registry = ChannelRegistry() outbound_plugin = self._make_partial_plugin("outbound-only") meta_only_plugin = MagicMock() meta_only_plugin.get_meta.return_value = ChannelMeta(channel_type="meta-only", display_name="Meta Only") registry.register(outbound_plugin) registry.register(meta_only_plugin) outbound_plugins = registry.get_plugins_by_port(OutboundPort) assert outbound_plugins == [outbound_plugin] def test_has_port_returns_true_for_implemented_port(self): registry = ChannelRegistry() plugin = self._make_partial_plugin("partial") registry.register(plugin) assert registry.has_port("partial", OutboundPort) is True def test_has_port_returns_false_for_missing_port(self): registry = ChannelRegistry() meta_only_plugin = MagicMock() meta_only_plugin.get_meta.return_value = ChannelMeta(channel_type="meta-only", display_name="Meta Only") registry.register(meta_only_plugin) assert registry.has_port("meta-only", OutboundPort) is False def test_get_plugins_by_port_dedupes_aliases(self): registry = ChannelRegistry() plugin = self._make_partial_plugin("aliased") plugin.get_meta().aliases = ["a1", "a2"] registry.register(plugin) outbound_plugins = registry.get_plugins_by_port(OutboundPort) assert len(outbound_plugins) == 1 assert outbound_plugins[0] is plugin