fix(transfer): 跳过整理的文件不再记录为入库成功 (#7283)

* fix(transfer): avoid successful history for skipped files

* test(transfer): fix regression test import order
This commit is contained in:
jxxghp
2026-11-03 13:24:37 +08:00
committed by GitHub
parent dc9eab695c
commit 26f066df53
3 changed files with 95 additions and 4 deletions

View File

@@ -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:

View File

@@ -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 == []

View File

@@ -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()