mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-08 17:08:35 +08:00
fix(plugin): allow dev local hot sync across system version gate (#5961)
This commit is contained in:
+5
-1
@@ -466,7 +466,8 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
|
|||||||
pid=plugin_dir_name,
|
pid=plugin_dir_name,
|
||||||
package_version=package_version,
|
package_version=package_version,
|
||||||
repo_path=local_repo_path,
|
repo_path=local_repo_path,
|
||||||
strict_compat=False
|
strict_compat=False,
|
||||||
|
strict_system_version=not settings.DEV
|
||||||
)
|
)
|
||||||
if candidate:
|
if candidate:
|
||||||
return candidate
|
return candidate
|
||||||
@@ -488,6 +489,9 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
|
|||||||
candidate = candidate or PluginHelper().get_local_plugin_candidate(pid)
|
candidate = candidate or PluginHelper().get_local_plugin_candidate(pid)
|
||||||
if not candidate:
|
if not candidate:
|
||||||
return False
|
return False
|
||||||
|
if candidate.get("compatible") is False:
|
||||||
|
logger.info(f"本地插件 {pid} 不满足同步条件,跳过自动同步:{candidate.get('skip_reason')}")
|
||||||
|
return False
|
||||||
|
|
||||||
source_dir = Path(candidate.get("path"))
|
source_dir = Path(candidate.get("path"))
|
||||||
dest_dir = settings.ROOT_PATH / "app" / "plugins" / pid.lower()
|
dest_dir = settings.ROOT_PATH / "app" / "plugins" / pid.lower()
|
||||||
|
|||||||
@@ -307,9 +307,11 @@ class PluginHelper(metaclass=WeakSingleton):
|
|||||||
|
|
||||||
def get_local_plugin_candidate(self, pid: str, package_version: Optional[str] = None,
|
def get_local_plugin_candidate(self, pid: str, package_version: Optional[str] = None,
|
||||||
repo_path: Optional[Path] = None,
|
repo_path: Optional[Path] = None,
|
||||||
strict_compat: bool = True) -> Optional[dict]:
|
strict_compat: bool = True,
|
||||||
|
strict_system_version: bool = True) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
获取指定插件ID的本地插件候选
|
获取指定插件ID的本地插件候选
|
||||||
|
:param strict_system_version: 是否将主系统版本范围不匹配视为不可用候选
|
||||||
"""
|
"""
|
||||||
if not pid:
|
if not pid:
|
||||||
return None
|
return None
|
||||||
@@ -352,7 +354,7 @@ class PluginHelper(metaclass=WeakSingleton):
|
|||||||
candidate["compatible"] = False
|
candidate["compatible"] = False
|
||||||
candidate["skip_reason"] = f"package.json 未声明 {settings.VERSION_FLAG} 兼容"
|
candidate["skip_reason"] = f"package.json 未声明 {settings.VERSION_FLAG} 兼容"
|
||||||
self.annotate_plugin_system_version(candidate)
|
self.annotate_plugin_system_version(candidate)
|
||||||
if candidate.get("system_version_compatible") is False:
|
if strict_system_version and candidate.get("system_version_compatible") is False:
|
||||||
candidate["compatible"] = False
|
candidate["compatible"] = False
|
||||||
candidate["skip_reason"] = candidate.get("system_version_message")
|
candidate["skip_reason"] = candidate.get("system_version_message")
|
||||||
if package_version is not None:
|
if package_version is not None:
|
||||||
@@ -369,6 +371,10 @@ class PluginHelper(metaclass=WeakSingleton):
|
|||||||
candidates = self.get_local_plugin_candidates()
|
candidates = self.get_local_plugin_candidates()
|
||||||
for candidate_pid, candidate in candidates.items():
|
for candidate_pid, candidate in candidates.items():
|
||||||
if candidate_pid.lower() == pid.lower():
|
if candidate_pid.lower() == pid.lower():
|
||||||
|
if strict_system_version and candidate.get("system_version_compatible") is False:
|
||||||
|
candidate = candidate.copy()
|
||||||
|
candidate["compatible"] = False
|
||||||
|
candidate["skip_reason"] = candidate.get("system_version_message")
|
||||||
return candidate
|
return candidate
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from typing import Iterator
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from packaging.version import Version
|
||||||
|
|
||||||
|
from app.core.plugin import PluginManager
|
||||||
|
from app.helper.plugin import PluginHelper
|
||||||
|
from app.schemas.types import SystemConfigKey
|
||||||
|
from app.utils.singleton import Singleton
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def plugin_manager() -> Iterator[PluginManager]:
|
||||||
|
"""构造隔离的插件管理器实例,避免单例状态污染其它用例。"""
|
||||||
|
Singleton._instances.pop((PluginManager, (), frozenset()), None)
|
||||||
|
manager = PluginManager()
|
||||||
|
yield manager
|
||||||
|
Singleton._instances.pop((PluginManager, (), frozenset()), None)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_local_plugin_repo(tmp_path: Path) -> tuple[Path, Path]:
|
||||||
|
"""构造带系统版本要求的本地 v2 插件仓库。"""
|
||||||
|
repo_path = tmp_path / "local-plugins"
|
||||||
|
source_dir = repo_path / "plugins.v2" / "demoplugin"
|
||||||
|
source_file = source_dir / "__init__.py"
|
||||||
|
source_dir.mkdir(parents=True)
|
||||||
|
source_file.write_text(
|
||||||
|
"from app.plugins import _PluginBase\n"
|
||||||
|
"class DemoPlugin(_PluginBase):\n"
|
||||||
|
" plugin_name = 'Demo'\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(repo_path / "package.v2.json").write_text(
|
||||||
|
'{"DemoPlugin": {"version": "1.0.0", "system_version": ">=2.13.11"}}',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return repo_path, source_file
|
||||||
|
|
||||||
|
|
||||||
|
def test_dev_local_plugin_candidate_keeps_hot_sync_allowed_when_system_version_lags(
|
||||||
|
tmp_path,
|
||||||
|
monkeypatch,
|
||||||
|
plugin_manager: PluginManager,
|
||||||
|
) -> None:
|
||||||
|
"""DEV 本地源码候选保留热同步资格,系统版本差异只作为兼容性提示。"""
|
||||||
|
repo_path, source_file = _build_local_plugin_repo(tmp_path)
|
||||||
|
runtime_dir = tmp_path / "app" / "plugins" / "demoplugin"
|
||||||
|
|
||||||
|
monkeypatch.setattr("app.core.plugin.settings", SimpleNamespace(DEV=True, ROOT_PATH=tmp_path))
|
||||||
|
monkeypatch.setattr("app.helper.plugin.settings.PLUGIN_LOCAL_REPO_PATHS", str(repo_path))
|
||||||
|
monkeypatch.setattr(PluginHelper, "get_current_system_version", lambda: Version("2.13.10"))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.core.plugin.SystemConfigOper.get",
|
||||||
|
lambda _self, key: ["DemoPlugin"] if key == SystemConfigKey.UserInstalledPlugins else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
candidate = plugin_manager._get_local_plugin_candidate_from_path(source_file)
|
||||||
|
|
||||||
|
assert candidate["system_version_compatible"] is False
|
||||||
|
assert candidate.get("compatible") is not False
|
||||||
|
assert plugin_manager._sync_local_plugin_if_installed("DemoPlugin", candidate)
|
||||||
|
assert (runtime_dir / "__init__.py").read_text(encoding="utf-8") == source_file.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_local_plugin_candidate_keeps_system_version_gate_outside_dev(
|
||||||
|
tmp_path,
|
||||||
|
monkeypatch,
|
||||||
|
plugin_manager: PluginManager,
|
||||||
|
) -> None:
|
||||||
|
"""非 DEV 本地候选继续受主系统版本门禁保护,避免自动热加载绕过安装约束。"""
|
||||||
|
repo_path, source_file = _build_local_plugin_repo(tmp_path)
|
||||||
|
|
||||||
|
monkeypatch.setattr("app.core.plugin.settings", SimpleNamespace(DEV=False, ROOT_PATH=tmp_path))
|
||||||
|
monkeypatch.setattr("app.helper.plugin.settings.PLUGIN_LOCAL_REPO_PATHS", str(repo_path))
|
||||||
|
monkeypatch.setattr(PluginHelper, "get_current_system_version", lambda: Version("2.13.10"))
|
||||||
|
|
||||||
|
candidate = plugin_manager._get_local_plugin_candidate_from_path(source_file)
|
||||||
|
|
||||||
|
assert candidate["system_version_compatible"] is False
|
||||||
|
assert candidate["compatible"] is False
|
||||||
|
assert "MoviePilot 版本 >=2.13.11" in candidate["skip_reason"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_local_plugin_sync_without_candidate_respects_system_version_gate(
|
||||||
|
tmp_path,
|
||||||
|
monkeypatch,
|
||||||
|
plugin_manager: PluginManager,
|
||||||
|
) -> None:
|
||||||
|
"""未传候选时的本地同步兜底查询也必须遵守系统版本门禁。"""
|
||||||
|
repo_path, _source_file = _build_local_plugin_repo(tmp_path)
|
||||||
|
runtime_dir = tmp_path / "app" / "plugins" / "demoplugin"
|
||||||
|
settings_stub = SimpleNamespace(
|
||||||
|
DEV=False,
|
||||||
|
ROOT_PATH=tmp_path,
|
||||||
|
VERSION_FLAG="v2",
|
||||||
|
PLUGIN_LOCAL_REPO_PATHS=str(repo_path),
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr("app.core.plugin.settings", settings_stub)
|
||||||
|
monkeypatch.setattr("app.helper.plugin.settings", settings_stub)
|
||||||
|
monkeypatch.setattr(PluginHelper, "get_current_system_version", lambda: Version("2.13.10"))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.core.plugin.SystemConfigOper.get",
|
||||||
|
lambda _self, key: ["DemoPlugin"] if key == SystemConfigKey.UserInstalledPlugins else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not plugin_manager._sync_local_plugin_if_installed("DemoPlugin")
|
||||||
|
assert not runtime_dir.exists()
|
||||||
Reference in New Issue
Block a user