mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
95 lines
3.3 KiB
Python
95 lines
3.3 KiB
Python
"""模块字符串方法契约清单的架构测试。"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from app.runtime.extensions.module.contracts import (
|
|
ModuleErrorPolicy,
|
|
ModuleExecutionMode,
|
|
ModuleResultAggregation,
|
|
diagnose_module_callable,
|
|
get_module_method_contract,
|
|
is_explicit_module_method,
|
|
list_explicit_module_contracts,
|
|
)
|
|
|
|
|
|
RUNTIME_BASELINE = (
|
|
Path(__file__).parent / "fixtures" / "architecture" / "runtime-contract-baseline.json"
|
|
)
|
|
|
|
|
|
def test_all_scanned_module_methods_resolve_a_contract() -> None:
|
|
"""架构快照中的所有字符串方法都必须能解析到稳定聚合规则。"""
|
|
payload = json.loads(RUNTIME_BASELINE.read_text(encoding="utf-8"))
|
|
methods = payload["run_module"]["methods"]
|
|
|
|
assert methods
|
|
for method in methods:
|
|
contract = get_module_method_contract(method)
|
|
assert isinstance(contract.aggregation, ModuleResultAggregation)
|
|
assert contract.plugin_short_circuit is True
|
|
|
|
|
|
def test_high_frequency_capability_families_are_explicit() -> None:
|
|
"""媒体发现、识别、存储和消息族不能退回未分类 legacy 契约。"""
|
|
expected_families = {
|
|
"async_tmdb_discover": "tmdb",
|
|
"async_douban_discover": "douban",
|
|
"bangumi_info": "bangumi",
|
|
"anilist_info": "anilist",
|
|
"recognize_media": "media-recognition",
|
|
"mediaserver_items": "media-server",
|
|
"list_files": "storage",
|
|
"finalize_message": "messaging",
|
|
"scheduler_job": "scheduling",
|
|
}
|
|
|
|
for method, family in expected_families.items():
|
|
assert is_explicit_module_method(method)
|
|
assert get_module_method_contract(method).family == family
|
|
|
|
|
|
def test_unknown_plugin_method_keeps_legacy_compatibility() -> None:
|
|
"""第三方插件自定义方法仍应落入开放的 legacy 调度协议。"""
|
|
contract = get_module_method_contract("third_party_custom_method")
|
|
|
|
assert contract.family == "legacy"
|
|
assert contract.supports_sync is True
|
|
assert contract.supports_async is True
|
|
|
|
|
|
def test_contract_v2_freezes_at_least_twenty_high_value_methods() -> None:
|
|
"""首批能力必须具备可生成文档和诊断的完整 V2 字段。"""
|
|
contracts = list_explicit_module_contracts()
|
|
|
|
assert len(contracts) >= 20
|
|
for contract in contracts.values():
|
|
assert contract.version == 1
|
|
assert contract.input_contract != "legacy_args"
|
|
assert contract.result_contract
|
|
assert contract.execution is ModuleExecutionMode.SYNC_OR_ASYNC
|
|
assert contract.timeout_policy == "caller_budget"
|
|
assert contract.error_policy is ModuleErrorPolicy.ISOLATE_PROVIDER
|
|
assert contract.public_to_plugins is True
|
|
|
|
|
|
def test_signature_diagnostics_do_not_reject_legacy_callable() -> None:
|
|
"""无法检查的旧插件 callable 只产生诊断,仍由 dispatcher 决定是否执行。"""
|
|
class _OpaqueCallable:
|
|
"""模拟 inspect 无法解析签名的第三方 callable。"""
|
|
|
|
@property
|
|
def __signature__(self):
|
|
"""模拟扩展对象不提供 Python signature。"""
|
|
raise ValueError("opaque")
|
|
|
|
def __call__(self):
|
|
"""保留可调用行为。"""
|
|
return "ok"
|
|
|
|
assert diagnose_module_callable("recognize_media", _OpaqueCallable()) == (
|
|
"signature-unavailable",
|
|
)
|
|
assert _OpaqueCallable()() == "ok"
|