fix(plugin): 按绑定仓库保留更新候选 (#6474)

* fix(plugin): preserve repository update candidates

* chore(ci): sync architecture ratchet baselines

* chore(ci): align canonical coverage baseline

* chore(ci): align coverage ratchet baseline
This commit is contained in:
InfinityPacer
2026-08-27 06:56:33 +08:00
committed by GitHub
parent 2553226f37
commit 627775c090
9 changed files with 152 additions and 9 deletions
+3 -3
View File
@@ -1,8 +1,8 @@
{
"application": {
"covered_lines": 9292,
"percent": 77.76,
"statements": 11949
"covered_lines": 9309,
"percent": 77.8,
"statements": 11965
},
"domain": {
"covered_lines": 3390,
+2 -1
View File
@@ -1462,7 +1462,8 @@
},
"app/chain/media.py": {
"arg-type": 34,
"assignment": 15,
"assignment": 16,
"call-overload": 1,
"comparison-overlap": 3,
"no-any-return": 3,
"no-untyped-def": 3,
+26
View File
@@ -113,6 +113,32 @@ async def test_async_collect_isolates_failure_and_completes_progress():
assert progress.call_args_list[-1].kwargs["value"] == 100
@pytest.mark.asyncio
async def test_async_collect_preserves_each_repository_update_candidate():
"""来源准入读取应保留每个仓库的最高版本,不能先按插件 ID 全局去重。"""
service = _service()
async def loader(market: str, package_version: str | None, _force: bool):
if package_version is None:
return []
version = "2.0.0" if market == "https://market-bound" else "3.0.0"
return [_plugin("Demo", version, market)]
result = await service.async_collect(
markets=["https://market-bound", "https://market-alternative"],
compatible_flags=["v3"],
force=False,
loader=loader,
preserve_sources=True,
)
assert [(plugin.repo_url, plugin.plugin_version) for plugin in result] == [
("https://market-bound", "2.0.0"),
("https://market-alternative", "3.0.0"),
]
assert service.merge(result, [], ["https://market-bound", "https://market-alternative"]) == [result[1]]
@pytest.mark.asyncio
async def test_async_collect_cancels_all_loaders_when_parent_is_cancelled():
"""请求取消时必须取消并回收全部市场 loader,不能把子任务遗留在事件循环。"""
+52
View File
@@ -187,6 +187,58 @@ def test_bound_repository_update_precedes_a_higher_alternative():
assert result[0].update_candidate.is_bound is True
def test_market_endpoint_reads_source_preserving_candidates_for_bound_update():
"""市场接口必须在全局版本合并前保留绑定仓库候选。"""
installed = schemas.Plugin(
id="DemoPlugin",
plugin_version="1.0.0",
installed=True,
)
bound_update = schemas.Plugin(
id="DemoPlugin",
plugin_version="2.0.0",
repo_url=SOURCE_URL,
has_update=True,
)
alternative_update = schemas.Plugin(
id="DemoPlugin",
plugin_version="3.0.0",
repo_url="https://github.com/jxxghp/MoviePilot-Plugins",
has_update=True,
)
plugin_manager = MagicMock()
plugin_manager.get_installed_plugins.return_value = [installed]
plugin_manager.get_local_plugins.return_value = []
plugin_manager.get_local_repo_plugins.return_value = []
plugin_manager.async_get_online_plugin_candidates = AsyncMock(
return_value=[bound_update, alternative_update]
)
plugin_manager.process_plugins_list.side_effect = (
lambda higher, base: [
max(
higher + base,
key=lambda plugin: tuple(
int(part) for part in plugin.plugin_version.split(".")
),
)
]
)
persistence = MagicMock()
persistence.list_identities = AsyncMock(return_value=[_plugin_identity()])
with (
patch("app.api.endpoints.plugin.get_plugin_manager", return_value=plugin_manager),
patch("app.api.endpoints.plugin.get_plugin_persistence", return_value=persistence),
):
result = asyncio.run(plugin_endpoint.all_plugins(None, "market", False))
assert result == [bound_update]
assert result[0].update_candidate is not None
assert result[0].update_candidate.version == "2.0.0"
assert result[0].update_candidate.is_bound is True
plugin_manager.async_get_online_plugin_candidates.assert_awaited_once_with(False)
def _persistence(identity: PluginIdentity) -> MagicMock:
"""构造只暴露身份读取合同的异步持久化替身。"""
persistence = MagicMock()