217 lines
8.3 KiB
Python
217 lines
8.3 KiB
Python
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
|