重大重构:引擎模块拆分 + 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:
Falck
2026-05-12 11:40:06 +08:00
parent 3a096f59a9
commit bce27db4ac
57 changed files with 3669 additions and 2367 deletions

View 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'])