新增了插件相关的完整领域模型、应用服务、基础设施实现,包括: 1. 插件状态、注册模式、来源等基础枚举和数据结构 2. 插件清单解析、发现、加载工具类 3. 插件注册表领域服务和内存存储实现 4. 插件相关的命令、查询、事件定义 5. 插件REST API接口和DTO映射 6. 集成了原有通道适配器到插件系统 7. 新增内置插件注册和自动发现能力
86 lines
2.5 KiB
Python
86 lines
2.5 KiB
Python
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
|