mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
refactor: enforce module fanout contracts
This commit is contained in:
+33
-26
@@ -3762,15 +3762,15 @@
|
||||
"version": 1
|
||||
},
|
||||
"clear_cache": {
|
||||
"aggregation": "legacy",
|
||||
"aggregation": "fan_out",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "category",
|
||||
"input_contract": "CategoryKeywordArguments",
|
||||
"plugin_short_circuit": true,
|
||||
"input_contract": "CacheClearRequest",
|
||||
"plugin_short_circuit": false,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "CategoryProviderResult",
|
||||
"result_contract": "None",
|
||||
"result_shape": "any",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
@@ -4011,15 +4011,19 @@
|
||||
"version": 1
|
||||
},
|
||||
"download_added": {
|
||||
"aggregation": "legacy",
|
||||
"aggregation": "fan_out",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "downloader",
|
||||
"input_contract": "DownloaderKeywordArguments",
|
||||
"plugin_short_circuit": true,
|
||||
"input_contract": "DownloadAddedHook",
|
||||
"plugin_short_circuit": false,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "DownloaderProviderResult",
|
||||
"required_parameters": [
|
||||
"context",
|
||||
"download_dir",
|
||||
"torrent_content"
|
||||
],
|
||||
"result_contract": "None",
|
||||
"result_shape": "any",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
@@ -5160,15 +5164,15 @@
|
||||
"version": 1
|
||||
},
|
||||
"music_cache_clear": {
|
||||
"aggregation": "legacy",
|
||||
"aggregation": "fan_out",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "music",
|
||||
"input_contract": "MusicKeywordArguments",
|
||||
"plugin_short_circuit": true,
|
||||
"input_contract": "MusicCacheClearRequest",
|
||||
"plugin_short_circuit": false,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "MusicProviderResult",
|
||||
"result_contract": "None",
|
||||
"result_shape": "any",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
@@ -5420,12 +5424,12 @@
|
||||
"version": 1
|
||||
},
|
||||
"register_commands": {
|
||||
"aggregation": "legacy",
|
||||
"aggregation": "fan_out",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "messaging",
|
||||
"input_contract": "CommandRegistrationRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"plugin_short_circuit": false,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [
|
||||
"commands"
|
||||
@@ -5495,12 +5499,12 @@
|
||||
"version": 1
|
||||
},
|
||||
"scheduler_job": {
|
||||
"aggregation": "legacy",
|
||||
"aggregation": "fan_out",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "scheduling",
|
||||
"input_contract": "SchedulerJobRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"plugin_short_circuit": false,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "None",
|
||||
@@ -5798,15 +5802,15 @@
|
||||
"version": 1
|
||||
},
|
||||
"tmdb_cache_clear": {
|
||||
"aggregation": "legacy",
|
||||
"aggregation": "fan_out",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "tmdb",
|
||||
"input_contract": "TmdbKeywordArguments",
|
||||
"plugin_short_circuit": true,
|
||||
"input_contract": "TmdbCacheClearRequest",
|
||||
"plugin_short_circuit": false,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "TmdbProviderResult",
|
||||
"result_contract": "None",
|
||||
"result_shape": "any",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
@@ -6184,15 +6188,18 @@
|
||||
"version": 1
|
||||
},
|
||||
"transfer_completed": {
|
||||
"aggregation": "legacy",
|
||||
"aggregation": "fan_out",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "downloader",
|
||||
"input_contract": "DownloaderKeywordArguments",
|
||||
"plugin_short_circuit": true,
|
||||
"input_contract": "TransferCompletedHook",
|
||||
"plugin_short_circuit": false,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "DownloaderProviderResult",
|
||||
"required_parameters": [
|
||||
"downloader",
|
||||
"hashs"
|
||||
],
|
||||
"result_contract": "None",
|
||||
"result_shape": "any",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
|
||||
@@ -94,6 +94,56 @@ def test_plugin_scalar_short_circuits_system_modules() -> None:
|
||||
system_call.assert_not_called()
|
||||
|
||||
|
||||
def test_fan_out_contract_runs_every_provider_and_ignores_results() -> None:
|
||||
"""副作用广播应执行全部插件和宿主 provider,并稳定返回 None。"""
|
||||
calls = []
|
||||
|
||||
def record(name: str, result):
|
||||
"""生成记录调用顺序并返回测试哨兵的 provider。"""
|
||||
return lambda: calls.append(name) or result
|
||||
|
||||
system_20 = _Module("系统二", 20, record("system-20", "ignored-system"))
|
||||
system_10 = _Module("系统一", 10, record("system-10", None))
|
||||
setattr(system_20, "clear_cache", system_20.execute)
|
||||
setattr(system_10, "clear_cache", system_10.execute)
|
||||
dispatcher, _, _, _ = _dispatcher(
|
||||
plugins={
|
||||
("P1", "插件一"): {"clear_cache": record("plugin-1", "ignored-plugin")},
|
||||
("P2", "插件二"): {"clear_cache": record("plugin-2", None)},
|
||||
},
|
||||
modules=[system_20, system_10],
|
||||
)
|
||||
|
||||
assert dispatcher.dispatch("clear_cache") is None
|
||||
assert calls == ["plugin-1", "plugin-2", "system-10", "system-20"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_fan_out_contract_matches_sync_execution() -> None:
|
||||
"""异步广播也应忽略返回值并执行全部同步或异步 provider。"""
|
||||
calls = []
|
||||
|
||||
async def plugin_call():
|
||||
"""记录异步插件调用并返回应被忽略的哨兵。"""
|
||||
calls.append("plugin")
|
||||
return "ignored-plugin"
|
||||
|
||||
def system_call():
|
||||
"""记录同步宿主调用并返回应被忽略的哨兵。"""
|
||||
calls.append("system")
|
||||
return "ignored-system"
|
||||
|
||||
module = _Module("系统", 10, system_call)
|
||||
setattr(module, "clear_cache", module.execute)
|
||||
dispatcher, _, _, _ = _dispatcher(
|
||||
plugins={("P1", "插件一"): {"clear_cache": plugin_call}},
|
||||
modules=[module],
|
||||
)
|
||||
|
||||
assert await dispatcher.async_dispatch("clear_cache") is None
|
||||
assert calls == ["plugin", "system"]
|
||||
|
||||
|
||||
def test_list_results_merge_in_plugin_then_priority_order() -> None:
|
||||
"""列表结果应先按插件顺序合并,再按宿主优先级继续合并。"""
|
||||
calls = []
|
||||
|
||||
@@ -34,7 +34,7 @@ def test_all_scanned_host_module_methods_have_explicit_v2_contracts() -> None:
|
||||
assert isinstance(contract.aggregation, ModuleResultAggregation)
|
||||
assert contract.input_contract != "legacy_args"
|
||||
assert contract.result_contract != "Any"
|
||||
assert contract.plugin_short_circuit is True
|
||||
assert isinstance(contract.plugin_short_circuit, bool)
|
||||
|
||||
|
||||
def test_high_frequency_capability_families_are_explicit() -> None:
|
||||
@@ -298,6 +298,25 @@ def test_torrent_filter_contract_preserves_original_argument_list_merge() -> Non
|
||||
)
|
||||
|
||||
|
||||
def test_side_effect_hooks_use_non_short_circuiting_fan_out_contracts() -> None:
|
||||
"""副作用钩子必须执行全部 provider,不能被任意返回值提前截断。"""
|
||||
methods = {
|
||||
"clear_cache",
|
||||
"download_added",
|
||||
"music_cache_clear",
|
||||
"register_commands",
|
||||
"scheduler_job",
|
||||
"tmdb_cache_clear",
|
||||
"transfer_completed",
|
||||
}
|
||||
|
||||
for method in methods:
|
||||
contract = get_module_method_contract(method)
|
||||
assert contract.aggregation is ModuleResultAggregation.FAN_OUT
|
||||
assert contract.result_contract == "None"
|
||||
assert contract.plugin_short_circuit is False
|
||||
|
||||
|
||||
def test_heterogeneous_torrent_files_result_remains_legacy_compatible() -> None:
|
||||
"""下载器文件集合尚未归一前不得声明虚假的列表聚合语义。"""
|
||||
contract = get_module_method_contract("torrent_files")
|
||||
|
||||
Reference in New Issue
Block a user