diff --git a/app/runtime/extensions/module/contracts.py b/app/runtime/extensions/module/contracts.py index 4f434d2da..b85aa8fbb 100644 --- a/app/runtime/extensions/module/contracts.py +++ b/app/runtime/extensions/module/contracts.py @@ -100,15 +100,20 @@ _METHOD_CONTRACTS = { "mediaserver_librarys": ModuleMethodContract(family="media-server", input_contract="MediaServerLibrariesRequest", result_contract="list[MediaServerLibrary]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("server", "username", "hidden")), "mediaserver_playing": ModuleMethodContract(family="media-server", input_contract="MediaServerRecentRequest", result_contract="list[MediaServerPlayItem]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("server", "count", "username")), "mediaserver_season_episode_ids": ModuleMethodContract(family="media-server", input_contract="MediaServerSeasonEpisodesRequest", result_contract="dict[int, str] | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("server", "item_id", "season")), + "any_files": ModuleMethodContract(family="storage", input_contract="StorageAnyFilesRequest", result_contract="bool | None", result_shape=ModuleResultShape.BOOLEAN, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem", "extensions")), + "create_folder": ModuleMethodContract(family="storage", input_contract="StorageCreateFolderRequest", result_contract="FileItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem", "name")), + "delete_file": ModuleMethodContract(family="storage", input_contract="StorageDeleteRequest", result_contract="bool | None", result_shape=ModuleResultShape.BOOLEAN, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem",)), "download_file": ModuleMethodContract(family="storage", input_contract="StorageDownloadRequest", result_contract="FileItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem", "path")), "upload_file": ModuleMethodContract(family="storage", input_contract="StorageUploadRequest", result_contract="FileItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem", "path", "new_name")), "list_files": ModuleMethodContract(family="storage", input_contract="StorageListRequest", result_contract="list[FileItem]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("fileitem", "recursion")), + "media_files": ModuleMethodContract(family="storage", input_contract="MediaFilesRequest", result_contract="list[FileItem]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("mediainfo",)), "get_file_item": ModuleMethodContract(family="storage", input_contract="StorageItemRequest", result_contract="FileItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("storage", "path")), "get_folder": ModuleMethodContract(family="storage", input_contract="StorageFolderRequest", result_contract="FileItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("storage", "path")), "get_parent_item": ModuleMethodContract(family="storage", input_contract="StorageParentRequest", result_contract="FileItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem",)), "rename_file": ModuleMethodContract(family="storage", input_contract="StorageRenameRequest", result_contract="bool | FileItem", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem", "name")), "storage_manage": ModuleMethodContract(family="storage", input_contract="StorageManageRequest", result_contract="StorageProviderResult", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("storage", "action")), "snapshot_storage": ModuleMethodContract(family="storage", input_contract="StorageSnapshotRequest", result_contract="dict[str, dict] | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("storage", "path", "last_snapshot_time", "max_depth", "previous_snapshot")), + "transfer": ModuleMethodContract(family="storage", input_contract="TransferRequest", result_contract="TransferInfo | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem", "meta", "mediainfo", "target_directory", "target_storage", "target_path", "transfer_type", "scrape", "library_type_folder", "library_category_folder", "episodes_info", "source_oper", "target_oper", "preview")), "send_message": ModuleMethodContract(family="messaging", input_contract="MessageSendRequest", result_contract="Message | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY), "finalize_message": ModuleMethodContract(family="messaging", input_contract="MessageFinalizeRequest", result_contract="Message | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("response",)), "register_commands": ModuleMethodContract(family="messaging", input_contract="CommandRegistrationRequest", result_contract="None", required_parameters=("commands",)), diff --git a/tests/fixtures/architecture/runtime-contract-baseline.json b/tests/fixtures/architecture/runtime-contract-baseline.json index 2bb4183bc..2f93cea39 100644 --- a/tests/fixtures/architecture/runtime-contract-baseline.json +++ b/tests/fixtures/architecture/runtime-contract-baseline.json @@ -2476,16 +2476,19 @@ "version": 1 }, "any_files": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "storage", - "input_contract": "StorageKeywordArguments", + "input_contract": "StorageAnyFilesRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "StorageProviderResult", - "result_shape": "any", + "required_parameters": [ + "extensions", + "fileitem" + ], + "result_contract": "bool | None", + "result_shape": "boolean", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", @@ -3576,15 +3579,18 @@ "version": 1 }, "create_folder": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "storage", - "input_contract": "StorageKeywordArguments", + "input_contract": "StorageCreateFolderRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "StorageProviderResult", + "required_parameters": [ + "fileitem", + "name" + ], + "result_contract": "FileItem | None", "result_shape": "any", "supports_async": true, "supports_sync": true, @@ -3592,16 +3598,18 @@ "version": 1 }, "delete_file": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "storage", - "input_contract": "StorageKeywordArguments", + "input_contract": "StorageDeleteRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "StorageProviderResult", - "result_shape": "any", + "required_parameters": [ + "fileitem" + ], + "result_contract": "bool | None", + "result_shape": "boolean", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", @@ -4405,16 +4413,18 @@ "version": 1 }, "media_files": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "storage", - "input_contract": "StorageKeywordArguments", + "input_contract": "MediaFilesRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "StorageProviderResult", - "result_shape": "any", + "required_parameters": [ + "mediainfo" + ], + "result_contract": "list[FileItem]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", @@ -5707,15 +5717,30 @@ "version": 1 }, "transfer": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "storage", - "input_contract": "StorageKeywordArguments", + "input_contract": "TransferRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "StorageProviderResult", + "required_parameters": [ + "episodes_info", + "fileitem", + "library_category_folder", + "library_type_folder", + "mediainfo", + "meta", + "preview", + "scrape", + "source_oper", + "target_directory", + "target_oper", + "target_path", + "target_storage", + "transfer_type" + ], + "result_contract": "TransferInfo | None", "result_shape": "any", "supports_async": true, "supports_sync": true, diff --git a/tests/test_module_method_contracts.py b/tests/test_module_method_contracts.py index 408d768ea..edfed6b24 100644 --- a/tests/test_module_method_contracts.py +++ b/tests/test_module_method_contracts.py @@ -189,6 +189,20 @@ def test_media_server_contracts_distinguish_streams_lists_and_scalar_routes() -> assert items.result_shape is ModuleResultShape.ANY +def test_storage_value_contracts_use_explicit_provider_semantics() -> None: + """存储值查询和动作应停止 legacy 接力,仅文件清单合并 provider 结果。""" + for method in ("any_files", "create_folder", "delete_file", "transfer"): + contract = get_module_method_contract(method) + assert contract.family == "storage" + assert contract.aggregation is ModuleResultAggregation.FIRST_NON_EMPTY + assert contract.required_parameters + + media_files = get_module_method_contract("media_files") + assert media_files.aggregation is ModuleResultAggregation.ORDERED_LIST_MERGE + assert media_files.result_shape is ModuleResultShape.LIST + assert media_files.required_parameters == ("mediainfo",) + + def test_heterogeneous_torrent_files_result_remains_legacy_compatible() -> None: """下载器文件集合尚未归一前不得声明虚假的列表聚合语义。""" contract = get_module_method_contract("torrent_files")