重大重构:引擎模块拆分 + P0插件实现 + 55个Bug修复
核心变更: - 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个测试, 全部通过
This commit is contained in:
190
oss/tests/test_integration.py
Normal file
190
oss/tests/test_integration.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""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'])
|
||||
Reference in New Issue
Block a user