核心变更: - engine.py(1781行)拆分为8个独立模块: lifecycle/security/deps/ datastore/pl_injector/watcher/signature/manager - 新增plugin-bridge: 事件总线 + 服务注册 + RPC通信 - 新增i18n: 国际化/多语言翻译支持 - 新增plugin-storage: 插件键值/文件存储 - 新增ws-api: WebSocket实时通信(pub/sub + 自定义处理器) - nodejs-adapter统一为Plugin ABC模式 Bug修复: - 修复load_all()中store_dir未定义崩溃 - 修复DependencyResolver入度计算(拓扑排序) - 修复PermissionError隐藏内置异常 - 修复CORS中间件头部未附加到响应 - 修复IntegrityChecker跳过__pycache__目录 - 修复版本号不一致(v2.0.0→v1.2.0) - 修复测试文件的Logger导入/路径/私有方法调用 - 修复context.py缺少typing导入 - 修复config.py STORE_DIR默认路径(./mods→./store) 测试覆盖: 14→91个测试, 全部通过
191 lines
6.6 KiB
Python
191 lines
6.6 KiB
Python
"""End-to-end integration tests for NebulaShell plugin system"""
|
|
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import json
|
|
import shutil
|
|
import pytest
|
|
from pathlib import Path
|
|
|
|
|
|
def _create_dummy_plugin(store_dir: str, name: str, dependencies: list = None, extra: str = ""):
|
|
plugin_dir = Path(store_dir) / "NebulaShell" / name
|
|
plugin_dir.mkdir(parents=True, exist_ok=True)
|
|
manifest = {
|
|
"metadata": {"name": name, "version": "1.0.0", "description": f"{name} plugin", "author": "test"},
|
|
"config": {"enabled": True, "args": {}},
|
|
"dependencies": dependencies or [],
|
|
"permissions": ["*"],
|
|
}
|
|
(plugin_dir / "manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
|
main_code = f"""class {name.capitalize().replace('-', '')}:
|
|
name = "{name}"
|
|
version = "1.0.0"
|
|
description = "{name} plugin"
|
|
def init(self, deps=None):
|
|
pass
|
|
def start(self):
|
|
pass
|
|
def stop(self):
|
|
pass
|
|
{extra}
|
|
|
|
def New():
|
|
return {name.capitalize().replace('-', '')}()
|
|
"""
|
|
(plugin_dir / "main.py").write_text(main_code, encoding="utf-8")
|
|
|
|
|
|
class TestIntegration:
|
|
@pytest.fixture
|
|
def temp_store(self):
|
|
tmp = tempfile.mkdtemp()
|
|
store = Path(tmp) / "store"
|
|
store.mkdir()
|
|
(store / "NebulaShell").mkdir()
|
|
yield str(store)
|
|
shutil.rmtree(tmp)
|
|
|
|
def test_plugin_manager_create(self):
|
|
from oss.core.manager import PluginManager
|
|
pm = PluginManager()
|
|
assert pm is not None
|
|
assert pm.plugins == {}
|
|
|
|
def test_load_single_plugin(self, temp_store):
|
|
_create_dummy_plugin(temp_store, "hello-world")
|
|
from oss.core.manager import PluginManager
|
|
pm = PluginManager()
|
|
pm.load(Path(temp_store) / "NebulaShell" / "hello-world")
|
|
assert "hello-world" in pm.plugins
|
|
|
|
def test_load_plugins_with_dependencies(self, temp_store):
|
|
_create_dummy_plugin(temp_store, "base")
|
|
_create_dummy_plugin(temp_store, "dependent", dependencies=["base"])
|
|
from oss.core.manager import PluginManager
|
|
pm = PluginManager()
|
|
pm.load(Path(temp_store) / "NebulaShell" / "base")
|
|
pm.load(Path(temp_store) / "NebulaShell" / "dependent")
|
|
pm._sort_by_dependencies()
|
|
assert "base" in pm.plugins
|
|
assert "dependent" in pm.plugins
|
|
|
|
def test_init_and_start_all(self, temp_store):
|
|
_create_dummy_plugin(temp_store, "test-me", extra="""
|
|
_started = False
|
|
def is_started(self):
|
|
return self._started
|
|
def start(self):
|
|
self._started = True
|
|
""")
|
|
from oss.core.manager import PluginManager
|
|
pm = PluginManager()
|
|
pm.load(Path(temp_store) / "NebulaShell" / "test-me")
|
|
pm.init_and_start_all()
|
|
instance = pm.plugins["test-me"]["instance"]
|
|
assert instance.is_started() is True
|
|
|
|
def test_load_all_from_dir(self, temp_store):
|
|
_create_dummy_plugin(temp_store, "alpha")
|
|
_create_dummy_plugin(temp_store, "beta")
|
|
from oss.core.manager import PluginManager
|
|
from oss.config import init_config
|
|
init_config()
|
|
pm = PluginManager()
|
|
pm._load_plugins_from_dir(Path(temp_store))
|
|
assert "alpha" in pm.plugins
|
|
assert "beta" in pm.plugins
|
|
|
|
def test_stop_all(self, temp_store):
|
|
_create_dummy_plugin(temp_store, "will-stop", extra="""
|
|
_stopped = False
|
|
def is_stopped(self):
|
|
return self._stopped
|
|
def stop(self):
|
|
self._stopped = True
|
|
""")
|
|
from oss.core.manager import PluginManager
|
|
pm = PluginManager()
|
|
pm.load(Path(temp_store) / "NebulaShell" / "will-stop")
|
|
pm.stop_all()
|
|
instance = pm.plugins["will-stop"]["instance"]
|
|
assert instance.is_stopped() is True
|
|
|
|
def test_plugin_manager_status(self, temp_store):
|
|
_create_dummy_plugin(temp_store, "status-test")
|
|
from oss.core.manager import PluginManager
|
|
pm = PluginManager()
|
|
pm.load(Path(temp_store) / "NebulaShell" / "status-test")
|
|
status = pm.get_status()
|
|
assert status["plugins"]["total"] == 1
|
|
|
|
def test_dependency_resolver(self):
|
|
from oss.core.deps import DependencyResolver
|
|
dr = DependencyResolver()
|
|
dr.add_dependency("a", ["b"])
|
|
dr.add_dependency("b", ["c"])
|
|
dr.add_dependency("c", [])
|
|
order = dr.resolve()
|
|
assert order.index("c") < order.index("b") < order.index("a")
|
|
|
|
def test_plugin_info(self):
|
|
from oss.core.manager import PluginInfo
|
|
info = PluginInfo()
|
|
info.name = "test"
|
|
assert info.name == "test"
|
|
|
|
def test_plugin_proxy_permission(self):
|
|
from oss.core.manager import PluginInfo
|
|
from oss.core.security import PluginProxy, PluginPermissionError
|
|
proxy = PluginProxy("caller", object(), ["allowed"], {"allowed": {"instance": object()}})
|
|
assert proxy.get_plugin("allowed") is not None
|
|
with pytest.raises(PluginPermissionError):
|
|
proxy.get_plugin("not-allowed")
|
|
|
|
def test_data_store_basic(self):
|
|
from oss.core.datastore import DataStore
|
|
import tempfile
|
|
ds = DataStore()
|
|
orig = ds._base_dir
|
|
tmp = Path(tempfile.mkdtemp())
|
|
ds._base_dir = tmp / "data"
|
|
ds._base_dir.mkdir(parents=True, exist_ok=True)
|
|
assert ds.save("test-plugin", "key", {"value": 42}) is True
|
|
loaded = ds.load("test-plugin", "key")
|
|
assert loaded == {"value": 42}
|
|
ds.delete("test-plugin", "key")
|
|
assert ds.load("test-plugin", "key") is None
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
|
|
def test_get_status_summary(self, temp_store):
|
|
_create_dummy_plugin(temp_store, "stat-p")
|
|
from oss.core.manager import PluginManager
|
|
pm = PluginManager()
|
|
pm.load(Path(temp_store) / "NebulaShell" / "stat-p")
|
|
s = pm.get_status()
|
|
assert isinstance(s, dict)
|
|
assert "plugins" in s
|
|
|
|
def test_capability_registry(self):
|
|
from oss.core.manager import CapabilityRegistry
|
|
cr = CapabilityRegistry()
|
|
cr.register_provider("http", "a", object())
|
|
assert cr.has_capability("http") is True
|
|
assert cr.get_provider("http") is not None
|
|
|
|
def test_get_ordered_plugins(self, temp_store):
|
|
_create_dummy_plugin(temp_store, "first")
|
|
_create_dummy_plugin(temp_store, "second")
|
|
from oss.core.manager import PluginManager
|
|
pm = PluginManager()
|
|
pm.load(Path(temp_store) / "NebulaShell" / "first")
|
|
pm.load(Path(temp_store) / "NebulaShell" / "second")
|
|
ordered = pm._get_ordered_plugins()
|
|
assert "first" in ordered
|
|
assert "second" in ordered
|
|
|
|
|
|
if __name__ == '__main__':
|
|
pytest.main([__file__, '-v'])
|