""" 插件 package 声明快照和值对象展示回退测试。 """ from datetime import datetime, timezone import pytest from app.application.plugin.catalog import apply_declared_metadata_fallback from app.application.plugin.declaration import PluginDeclaredMetadata from app.application.plugin.identity import ( PluginBindingBasis, PluginIdentity, PluginPayloadSourceType, TrustedPluginSourceType, ) from app.schemas.plugin import Plugin, PluginRuntimeStatus NOW = datetime(2026, 8, 25, 12, 0, tzinfo=timezone.utc) OFFICIAL_SOURCE = "github:jxxghp/moviepilot-plugins" def _metadata( package: dict[str, object], *, version: str = "1.0.0", matches_payload: bool = True, ) -> PluginDeclaredMetadata: """ 构造一份可用于身份测试的 package 声明快照。 """ return PluginDeclaredMetadata.from_package( package, declaration_version=version, manifest_matches_payload=matches_payload, ) def _identity( metadata: PluginDeclaredMetadata, *, version: str = "1.0.0", plugin_id: str = "DemoPlugin", ) -> PluginIdentity: """ 构造带已提交声明快照的官方在线身份。 """ return PluginIdentity( plugin_id=plugin_id, normalized_plugin_id=plugin_id.lower(), trusted_source_type=TrustedPluginSourceType.OFFICIAL, trusted_source_key=OFFICIAL_SOURCE, binding_basis=PluginBindingBasis.OFFICIAL_DEFAULT, payload_source_type=PluginPayloadSourceType.OFFICIAL, payload_source_key=OFFICIAL_SOURCE, declared_version=version, package_generation="v3", declared_metadata=metadata, payload_receipt="sha256:" + "0" * 64, revision=1, created_at=NOW, updated_at=NOW, bound_at=NOW, payload_applied_at=NOW, ) def test_declared_metadata_round_trips_package_and_storage_without_extra_fields() -> None: """ package 声明保存后应可稳定往返,并丢弃未纳入合同的字段。 """ package = { "name": "Demo", "description": "A demo plugin", "icon": "demo.svg", "author": "MoviePilot", "system_version": ">=3.0.0", "labels": "alpha, beta", "level": 2, "release": True, "v4": True, "v4t": False, "unknown_field": "ignored", "repo_url": "https://github.com/attacker/repo", "source": "github:attacker/repo", "token": "secret", "plugin_public_key": "public-key", } snapshot = _metadata(package, version="2.0.0") restored = PluginDeclaredMetadata.from_storage(snapshot.to_json()) stored = restored.to_json() assert restored == snapshot assert restored.declaration_version == "2.0.0" assert restored.manifest_matches_payload is True assert restored.runtime_support("v4") is True assert restored.runtime_support("v4t") is False assert stored["manifest"] == { "author": "MoviePilot", "description": "A demo plugin", "icon": "demo.svg", "labels": ["alpha", "beta"], "level": 2, "name": "Demo", "release": True, "system_version": ">=3.0.0", } assert stored["runtime"] == {"v4": True, "v4t": False} assert "unknown_field" not in str(stored) assert "attacker" not in str(stored) assert "secret" not in str(stored) assert "public-key" not in str(stored) def test_declared_metadata_preserves_system_version_storage_limit() -> None: """ 系统版本声明继续遵守旧数据库列的长度合同,保证迁移可降级。 """ with pytest.raises(ValueError, match="长度不能超过 128"): _metadata({"system_version": "v" * 129}) def test_declared_metadata_ignores_unknown_runtime_shapes_and_is_deep_copied() -> None: """ 未知代际和外部可变对象不能污染已提交快照。 """ labels = ["alpha"] package = { "name": "Demo", "labels": labels, "v4": True, "v4t": False, "v5": "not-a-bool", "vX": True, } snapshot = _metadata(package) labels.append("mutated-after-build") exported = snapshot.to_json() exported["manifest"]["labels"].append("mutated-export") assert snapshot.to_json()["manifest"]["labels"] == ["alpha"] assert snapshot.runtime_support("v4") is True assert snapshot.runtime_support("v4t") is False assert snapshot.runtime_support("v5") is None def test_declared_metadata_records_current_and_historical_release_truth() -> None: """ 当前 package 与历史 Release 的声明对应关系必须可区分。 """ current = _metadata({"name": "Demo", "v3": True}, version="2.0.0") historical = _metadata( {"name": "Demo", "v3": True}, version="1.0.0", matches_payload=False, ) assert current.manifest_matches_payload is True assert historical.manifest_matches_payload is False assert historical.declaration_version == "1.0.0" def test_declared_metadata_fallback_is_batch_safe_and_preserves_runtime_fields() -> None: """ 批量展示回退只补空展示字段,不覆盖运行态或另一插件身份。 """ first = _identity( _metadata( { "name": "Demo from snapshot", "description": "Saved description", "icon": "saved.svg", "author": "Saved author", "labels": ["saved", "plugin"], }, version="2.0.0", ), version="2.0.0", plugin_id="DemoPlugin", ) second = _identity( _metadata({"name": "Other from snapshot"}, version="3.0.0"), version="3.0.0", plugin_id="OtherPlugin", ) plugins = [ Plugin( id="DemoPlugin", plugin_name="DemoPlugin", plugin_version=None, runtime_status=PluginRuntimeStatus.LOAD_FAILED, state=True, plugin_desc=None, ), Plugin( id="OtherPlugin", plugin_name="Runtime name", plugin_desc="Runtime description", plugin_version="loaded-version", runtime_status=PluginRuntimeStatus.READY, ), Plugin(id="MissingPlugin", plugin_name="MissingPlugin"), ] result = apply_declared_metadata_fallback( plugins, {"demoplugin": first, "otherplugin": second}, ) assert result[0].plugin_name == "Demo from snapshot" assert result[0].plugin_desc == "Saved description" assert result[0].plugin_icon == "saved.svg" assert result[0].plugin_author == "Saved author" assert result[0].plugin_version == "2.0.0" assert result[0].plugin_label == "saved,plugin" assert result[0].runtime_status is PluginRuntimeStatus.LOAD_FAILED assert result[0].state is True assert result[1].plugin_name == "Runtime name" assert result[1].plugin_desc == "Runtime description" assert result[1].plugin_version == "loaded-version" assert result[1].runtime_status is PluginRuntimeStatus.READY assert result[2].plugin_name == "MissingPlugin" def test_installed_card_marks_runtime_incompatibility_from_declared_snapshot() -> None: """ 已安装卡片的 v3t 不兼容提示必须能离线得出。 市场候选那条标注路径要联网才有结果,而切到 v3t 的用户首先看到的是"我的插件"。 安装时提交的声明快照是运行目录唯一可离线信任的来源,这里钉住它被消费。 """ blocked = _identity( _metadata({"name": "Blocked", "v3": True, "v3t": False}), plugin_id="BlockedPlugin", ) allowed = _identity( _metadata({"name": "Allowed", "v3": True}), plugin_id="AllowedPlugin", ) plugins = [ Plugin(id="BlockedPlugin", plugin_name="Blocked", installed=True), Plugin(id="AllowedPlugin", plugin_name="Allowed", installed=True), ] on_free_threaded = apply_declared_metadata_fallback( plugins, {"blockedplugin": blocked, "allowedplugin": allowed}, free_threaded=True, ) assert on_free_threaded[0].runtime_compatible is False assert "v3t" in (on_free_threaded[0].runtime_message or "") assert on_free_threaded[1].runtime_compatible is not False # 标准 V3 上同一份声明不得产生任何提示 on_standard = apply_declared_metadata_fallback( plugins, {"blockedplugin": blocked, "allowedplugin": allowed}, free_threaded=False, ) assert on_standard[0].runtime_compatible is not False assert on_standard[0].runtime_message is None