mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-30 20:05:42 +08:00
173 lines
6.5 KiB
Python
173 lines
6.5 KiB
Python
"""贯通大小覆盖裁决与 durable 结算,防止重复整理降级成功历史。"""
|
|
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from app.application.history.retry import HistoryGateAction, evaluate_history_gate
|
|
from app.application.transfer.execution import TransferSettlementResult
|
|
from app.chain.transfer.execution import _DurableTransferStepRunner
|
|
from app.modules.filemanager.transhandler import TransHandler
|
|
from tests.test_transfer_job_manager import (
|
|
FakeMedia,
|
|
bind_terminal_checkpoint,
|
|
make_fileitem,
|
|
make_task,
|
|
make_transfer_chain,
|
|
)
|
|
|
|
|
|
def _resolve_size_overwrite(task, target_size):
|
|
"""调用真实大小覆盖分支,仅替换目标存储查询与插件事件边界。"""
|
|
task.mediainfo = FakeMedia()
|
|
target = make_fileitem("/library/Test.Show.S01E01.mkv", size=target_size)
|
|
storage = MagicMock()
|
|
storage.get_item_strict.return_value = target
|
|
result = TransHandler()._TransHandler__resolve_overwrite(
|
|
fileitem=task.fileitem,
|
|
meta=task.meta,
|
|
mediainfo=task.mediainfo,
|
|
target_oper=storage,
|
|
target_storage="local",
|
|
target_file=Path(target.path),
|
|
transfer_type="link",
|
|
overwrite_mode="size",
|
|
need_notify=True,
|
|
)
|
|
storage.get_item_strict.assert_called_once_with(Path(target.path))
|
|
return result
|
|
|
|
|
|
@pytest.fixture
|
|
def overwrite_event(monkeypatch):
|
|
"""隔离插件覆盖事件,保留宿主大小策略的真实判断。"""
|
|
event = MagicMock(return_value=None)
|
|
monkeypatch.setattr(
|
|
"app.modules.filemanager.transhandler.eventmanager.send_event", event
|
|
)
|
|
return event
|
|
|
|
|
|
@pytest.mark.parametrize("target_size", [512, 1024, 2048])
|
|
def test_video_size_overwrite_marks_policy_decline(target_size, overwrite_event):
|
|
"""仅更大的源文件允许升级,同大小与更小文件应携带明确覆盖跳过事实。"""
|
|
task = make_task(1)
|
|
over_flag, latest, result = _resolve_size_overwrite(task, target_size)
|
|
|
|
assert latest is False
|
|
overwrite_event.assert_called_once()
|
|
if target_size < task.fileitem.size:
|
|
assert over_flag is True
|
|
assert result is None
|
|
else:
|
|
assert over_flag is False
|
|
assert result.success is False
|
|
assert result.overwrite_skipped is True
|
|
assert result.target_item.size == target_size
|
|
assert result.message == "媒体库存在同名文件,且质量更好"
|
|
|
|
|
|
@pytest.mark.parametrize("target_size", [1024, 2048])
|
|
def test_repeated_size_decline_preserves_successful_settlement(
|
|
target_size, overwrite_event, monkeypatch,
|
|
):
|
|
"""mtime 变化重送后的真实大小裁决必须保留成功历史并静默结算。"""
|
|
task = make_task(1)
|
|
task.fileitem.modify_time = 150.0
|
|
history = SimpleNamespace(
|
|
id=99,
|
|
status=True,
|
|
src_fileitem={"size": task.fileitem.size, "modify_time": 100.0},
|
|
)
|
|
assert evaluate_history_gate(
|
|
history,
|
|
file_size=task.fileitem.size,
|
|
file_modify_time=task.fileitem.modify_time,
|
|
) == HistoryGateAction.PASS_SIZE_CHANGED
|
|
_, _, result = _resolve_size_overwrite(task, target_size)
|
|
overwrite_event.assert_called_once()
|
|
|
|
runner = object.__new__(_DurableTransferStepRunner)
|
|
runner._task_id = "size-overwrite-task"
|
|
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"] == "overwrite_skipped"
|
|
bind_terminal_checkpoint(task, result)
|
|
task.bind_execution_checkpoint(checkpoint)
|
|
|
|
chain = make_transfer_chain()
|
|
chain.eventmanager = MagicMock()
|
|
chain.post_message = MagicMock()
|
|
chain.queue_failed_transfer_notification = MagicMock()
|
|
chain.transfer_history_repository.get_by_src.return_value = history
|
|
assert chain._TransferChain__put_to_jobview(task)
|
|
staging = MagicMock()
|
|
staging.get_success_by_src.return_value = history
|
|
|
|
def settle(**kwargs):
|
|
"""核实 writer 回读旧历史,并以无通知的成功终态结算。"""
|
|
assert kwargs["topic"] is None
|
|
assert kwargs["publish"] is None
|
|
assert kwargs["settlement"].outcome == "succeeded"
|
|
assert kwargs["settlement"].error is None
|
|
assert kwargs["stage_history"](staging) is history
|
|
return TransferSettlementResult(
|
|
history_id=history.id, settlement_revision=1, pending_deleted=True,
|
|
)
|
|
|
|
chain.durable_event_writer.transfer_result.side_effect = settle
|
|
add_fail = MagicMock()
|
|
record_failure = MagicMock()
|
|
monkeypatch.setattr("app.chain.transfer.settlement.add_transfer_fail", add_fail)
|
|
monkeypatch.setattr(
|
|
"app.chain.transfer.settlement.record_transfer_failure", record_failure
|
|
)
|
|
state, message = chain._TransferChain__default_callback(task, result)
|
|
|
|
assert state is False
|
|
assert message == result.message
|
|
assert task.terminal_settled is True
|
|
assert history.status is True
|
|
staging.get_success_by_src.assert_called_once_with(
|
|
task.fileitem.path, task.fileitem.storage,
|
|
)
|
|
chain.durable_event_writer.transfer_result.assert_called_once()
|
|
add_fail.assert_not_called()
|
|
record_failure.assert_not_called()
|
|
chain.eventmanager.send_event.assert_not_called()
|
|
chain.post_message.assert_not_called()
|
|
chain.queue_failed_transfer_notification.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize("history_status", [None, False])
|
|
def test_size_decline_without_success_history_keeps_failure(
|
|
history_status, overwrite_event,
|
|
):
|
|
"""目标来自其他源或只有失败历史时,大小拒绝仍按失败处理。"""
|
|
task = make_task(1)
|
|
_, _, result = _resolve_size_overwrite(task, task.fileitem.size)
|
|
overwrite_event.assert_called_once()
|
|
chain = make_transfer_chain()
|
|
chain.transfer_history_repository.get_by_src.return_value = (
|
|
None if history_status is None else SimpleNamespace(id=99, status=False)
|
|
)
|
|
chain.transfer_history_repository.get_success_by_src.return_value = None
|
|
bind_terminal_checkpoint(task, result)
|
|
declined = chain._is_overwrite_declined(
|
|
task, result, chain.transfer_history_repository,
|
|
)
|
|
settlement = chain._TransferChain__build_transfer_result_settlement(
|
|
task, result, overwrite_declined=declined,
|
|
)
|
|
|
|
assert declined is False
|
|
assert settlement.outcome == "failed"
|
|
assert settlement.error == result.message
|