From 26f066df53704a77e1d12480ff43f0a5d3f8f76f Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 3 Nov 2026 13:24:37 +0800 Subject: [PATCH] =?UTF-8?q?fix(transfer):=20=E8=B7=B3=E8=BF=87=E6=95=B4?= =?UTF-8?q?=E7=90=86=E7=9A=84=E6=96=87=E4=BB=B6=E4=B8=8D=E5=86=8D=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E4=B8=BA=E5=85=A5=E5=BA=93=E6=88=90=E5=8A=9F=20(#7283?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(transfer): avoid successful history for skipped files * test(transfer): fix regression test import order --- app/modules/filemanager/transhandler.py | 6 +- tests/test_filemanager_planning.py | 6 +- tests/test_transhandler_special_extra.py | 87 +++++++++++++++++++++++- 3 files changed, 95 insertions(+), 4 deletions(-) diff --git a/app/modules/filemanager/transhandler.py b/app/modules/filemanager/transhandler.py index 21ed0246f..c127ec223 100644 --- a/app/modules/filemanager/transhandler.py +++ b/app/modules/filemanager/transhandler.py @@ -1843,12 +1843,16 @@ class TransHandler: if checkpoint.skip_reason: logger.info(f"文件 {fileitem.path} 跳过整理:{checkpoint.skip_reason}") + # 没有执行文件转移,不能生成成功入库历史或触发成功后的清理。 return TransferInfo( - success=True, + success=False, message=checkpoint.skip_reason, fileitem=fileitem, + fail_list=[fileitem.path] if fileitem.path else [], transfer_type=transfer_type, need_notify=False, + failure_stage="planning", + recovery_action="检查源目录;特典请核对季集编号并补充文件名中的 SxxExx 标记后重新整理", ) if checkpoint.preview: diff --git a/tests/test_filemanager_planning.py b/tests/test_filemanager_planning.py index 2672d8d3b..17cc9c5d3 100644 --- a/tests/test_filemanager_planning.py +++ b/tests/test_filemanager_planning.py @@ -660,7 +660,11 @@ def test_empty_directory_plan_is_zero_operation_without_intercept_or_cleanup( cleanup_media_file=lambda _fileitem: calls.append("cleanup") or True, ) - assert result.success is True + assert result.success is False + assert result.message == "源目录中没有可整理文件" + assert result.failure_stage == "planning" + assert result.target_item is None + assert result.need_notify is False assert calls == [] diff --git a/tests/test_transhandler_special_extra.py b/tests/test_transhandler_special_extra.py index 55793edb1..b6c6ea97b 100644 --- a/tests/test_transhandler_special_extra.py +++ b/tests/test_transhandler_special_extra.py @@ -3,17 +3,21 @@ """ from pathlib import Path +from types import SimpleNamespace from unittest.mock import MagicMock import pytest +from app.application.transfer.execution import TransferSettlementResult from app.application.transfer.workflow import TransferPlanningInput +from app.chain.transfer.execution import _DurableTransferStepRunner from app.domain.context import MediaInfo from app.domain.meta.metavideo import MetaVideo from app.modules.filemanager.transhandler import TransHandler from app.schemas.file import FileItem from app.schemas.transfer import TransferInfo from app.schemas.types import MediaType +from tests.test_transfer_job_manager import FakeMedia, make_task, make_transfer_chain def _transfer_without_episode(file_name: str) -> tuple[TransferInfo, MagicMock, MagicMock]: @@ -92,15 +96,25 @@ def _transfer_without_episode(file_name: str) -> tuple[TransferInfo, MagicMock, "Test.Show.NCOP.mkv", "Test.Show.Menu.mkv", "Test.Show.Featurette.mkv", + *[ + f"[DMG&KNA&LKSUB&LoliHouse] Re Zero kara Hajimeru Isekai Seikatsu - SP{episode} " + "[WebRip 1080p HEVC-10bit AAC ASSx2].mkv" + for episode in range(26, 39) + ], ], ) def test_tv_special_extra_without_episode_skips_transfer(file_name: str) -> None: """ - 未识别出集数的已知特典文件应静默跳过并返回成功 + 未识别出集数的特典应保留跳过原因并返回未入库,且不执行存储操作。 """ result, source_oper, target_oper = _transfer_without_episode(file_name) - assert result.success is True + assert result.success is False + assert result.message == "未识别到文件集数,识别为特典/附加视频文件" + assert result.failure_stage == "planning" + assert "SxxExx" in result.recovery_action + assert result.target_item is None + assert result.fail_list == [f"/downloads/{file_name}"] assert result.need_notify is False assert result.fileitem.path == f"/downloads/{file_name}" assert source_oper.mock_calls == [] @@ -119,3 +133,72 @@ def test_regular_tv_file_without_episode_remains_failure() -> None: assert result.fail_list == ["/downloads/Test.Show.Bonus.mkv"] assert source_oper.mock_calls == [] assert target_oper.mock_calls == [] + + +@pytest.mark.parametrize("legacy", [False, True]) +def test_skipped_extra_settles_as_unsuccessful_history(monkeypatch, legacy: bool) -> None: + """ + 特典的零操作执行结果经普通及兼容结算均写未入库历史,且不触发成功副作用。 + """ + result, _, _ = _transfer_without_episode("Test.Show.SP33.mkv") + result.transfer_type = "move" + task = make_task(1) + task.fileitem = result.fileitem + task.meta = MetaVideo("Test.Show.SP33.mkv") + task.mediainfo = FakeMedia() + task.bind_admission_task_id("special-extra") + task.bind_execution_lease(owner_id="worker", lease_token="lease") + runner = object.__new__(_DurableTransferStepRunner) + runner._task_id = "special-extra" + runner._lease_token = "lease" + runner._operation_ids = [] + runner._command = MagicMock() + runner._command.checkpoint.side_effect = lambda **kwargs: SimpleNamespace( + checkpoint=kwargs["checkpoint"] + ) + checkpoint = runner.checkpoint(result) + assert checkpoint.payload["outcome"] == "failed" + assert checkpoint.operation_ids == () + assert checkpoint.skip_reason == result.message + task.bind_execution_checkpoint(checkpoint) + + chain = make_transfer_chain() + chain.jobview.add_task(task) + chain.transfer_history_repository.get_by_src.return_value = None + chain.queue_failed_transfer_notification = MagicMock() + chain._schedule_automatic_ai_takeover = MagicMock() + chain._TransferChain__forget_owned_lease = MagicMock() + chain._record_scrape_target = MagicMock() + chain.notify_successful_transfer = MagicMock() + chain._cleanup_successful_transfer = MagicMock() + chain.eventmanager = MagicMock() + staging = MagicMock() + staging.replace.return_value = SimpleNamespace(id=1) + + def settle(**kwargs): + """ + 执行真实历史字段投影,只替换数据库及事件写入边界。 + """ + assert kwargs["settlement"].outcome == "failed" + kwargs["stage_history"](staging) + return TransferSettlementResult(history_id=1, settlement_revision=1, pending_deleted=True) + + chain.durable_event_writer.transfer_result.side_effect = settle + monkeypatch.setattr("app.chain.transfer.settlement.record_transfer_failure", lambda *args, **kwargs: None) + if legacy: + chain._TransferChain__settle_legacy_transfer_result(task, result) + else: + state, message = chain._TransferChain__default_callback(task, result) + assert state is False + assert message == result.message + history = staging.replace.call_args.args[0] + assert history.status is False + assert history.errmsg == result.message + assert history.failure_stage == "planning" + assert history.recovery_action == result.recovery_action + assert history.dest is None + assert task.terminal_settled + chain.eventmanager.send_event.assert_not_called() + chain._record_scrape_target.assert_not_called() + chain.notify_successful_transfer.assert_not_called() + chain._cleanup_successful_transfer.assert_not_called()