from __future__ import annotations import sys import tempfile from pathlib import Path import pytest from yuxi.channel.infrastructure.plugin.loader import PluginLoader class FakeAdapterClass: def __init__(self, **kwargs) -> None: pass async def open(self) -> None: pass async def close(self) -> None: pass @property def capabilities(self): return None @classmethod def get_default_config(cls) -> dict: return {} class TestPluginLoader: @pytest.fixture def loader(self): return PluginLoader() @pytest.mark.asyncio async def test_load_module_by_path(self, loader): with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: f.write("class MyAdapter:\n") f.write(" def __init__(self, **kwargs): pass\n") f.write(" async def open(self): pass\n") f.write(" async def close(self): pass\n") f.write(" @property\n") f.write(" def capabilities(self): return None\n") f.write(" @classmethod\n") f.write(" def get_default_config(cls): return {}\n") path = f.name try: module = await loader.load(path) assert module is not None assert hasattr(module, "MyAdapter") finally: Path(path).unlink() @pytest.mark.asyncio async def test_load_existing_module(self, loader): module = await loader.load("sys") assert module is sys def test_extract_adapter_class_found(self, loader): module = type("Module", (), {"MyAdapter": FakeAdapterClass})() result = loader.extract_adapter_class(module) assert result is FakeAdapterClass def test_extract_adapter_class_none(self, loader): module = type("Module", (), {"foo": "bar"})() result = loader.extract_adapter_class(module) assert result is None def test_extract_adapter_class_skips_private(self, loader): class _PrivateAdapter(FakeAdapterClass): pass module = type("Module", (), {"_PrivateAdapter": _PrivateAdapter})() result = loader.extract_adapter_class(module) assert result is None def test_extract_adapter_class_requires_methods(self, loader): class Incomplete: pass module = type("Module", (), {"Incomplete": Incomplete})() result = loader.extract_adapter_class(module) assert result is None