mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-14 02:05:13 +08:00
* wip(v3): 移植监控与整理韧性修复到 v3 基线 包含:监控看门狗隔离/挂载探测、整理队列持久化、文件系统子进程代理、 写入原子化。迁移重挂到 v3 链 8a4c7e1d2f90 -> 7f5c1d2e3a4b -> e3d9f4b7c806。 tmdb 相关测试尚未通过,待定位。 * fix(v3): 修正移植引入的 16 项测试失败 - poller.py:合并时我方保留的行仍用旧变量名 merged_snapshot,而 v3 已统一 改名为 current_snapshot,导致 NameError 被外层 except 吞掉、快照从未保存 - smb.py:采纳 f-string 拆分写法,恢复 Python 3.11 可解析 - dispatcher 测试:历史查重由 _should_skip_by_history 统一承担,mock 点随之调整 - tmdb 缓存测试:补充 v3 新增的 media_source/media_id 字段 - tmdb 重试测试:为 fake 补充 match_multi/async_match_multi 尚余 3 项与 v3 识别流程的连接失败处理有关,待单独判断。 * fix(v3): 测试适配 v3 的 media_source/media_id 重构 v3 将媒体标识从 tmdbid 统一重构为 media_source + media_id,recognize_media 的 tmdbid 参数已被 **kwargs 静默吞掉——传了也不生效,流程会误降级到名称搜索。 tmdb 重试用例改用新参数后恢复正确路径。 同时修正 fake 的 match_multi 语义:真实实现(tmdbapi.match_multi)吞掉所有 异常并返回 None,连接失败与「未找到」在该路径上本就不可区分,fake 需保持一致。 至此移植引入的 19 项失败全部清零。 --------- Co-authored-by: Aqr-K <Aqr-K@users.noreply.github.com>
219 lines
7.4 KiB
Python
219 lines
7.4 KiB
Python
"""
|
||
覆盖「不覆盖裁决不应降级已有成功记录」的行为。
|
||
|
||
查重闸放行同路径新版本后,若 overwrite_mode 最终裁定不覆盖,媒体库中原有的
|
||
成功版本仍然在位——这是一次正常策略裁决而非整理故障。TransferChain 内部的
|
||
__is_overwrite_declined 用于识别这一场景,__default_callback 失败分支据此
|
||
决定是否写失败历史、发送失败事件与失败通知。本文件覆盖两者。
|
||
"""
|
||
from types import SimpleNamespace
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
from app.chain.transfer import TransferChain
|
||
from app.schemas import TransferInfo
|
||
from app.schemas.types import EventType
|
||
from tests.test_transfer_job_manager import FakeMedia, make_task, make_transfer_chain
|
||
|
||
|
||
def make_history_oper(history=None, success_history=None, raise_on_query: bool = False,
|
||
add_fail_calls=None):
|
||
"""构造 __is_overwrite_declined / __default_callback 查询与写入整理历史使用的替身。"""
|
||
|
||
def get_by_src(src, storage=None):
|
||
if raise_on_query:
|
||
raise RuntimeError("boom")
|
||
return history
|
||
|
||
def get_success_by_src(src, storage=None):
|
||
return success_history
|
||
|
||
def add_fail(**kwargs):
|
||
if add_fail_calls is not None:
|
||
add_fail_calls.append(kwargs)
|
||
return SimpleNamespace(id=1)
|
||
|
||
return SimpleNamespace(
|
||
get_by_src=get_by_src,
|
||
get_success_by_src=get_success_by_src,
|
||
add_fail=add_fail,
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# TransferChain.__is_overwrite_declined
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def test_overwrite_declined_false_when_flag_not_set():
|
||
"""overwrite_skipped 为假时直接判定为 False,且不应触发历史查询。"""
|
||
task = make_task(1)
|
||
transferinfo = TransferInfo(success=False, overwrite_skipped=False)
|
||
transferhis = make_history_oper(raise_on_query=True)
|
||
|
||
result = TransferChain._TransferChain__is_overwrite_declined(
|
||
task, transferinfo, transferhis
|
||
)
|
||
|
||
assert result is False
|
||
|
||
|
||
def test_overwrite_declined_true_when_success_history_exists():
|
||
"""overwrite_skipped 为真且同源已有成功记录时,应判定为保护场景。"""
|
||
task = make_task(1)
|
||
success_history = SimpleNamespace(id=1, status=True)
|
||
transferinfo = TransferInfo(success=False, overwrite_skipped=True)
|
||
transferhis = make_history_oper(history=success_history)
|
||
|
||
result = TransferChain._TransferChain__is_overwrite_declined(
|
||
task, transferinfo, transferhis
|
||
)
|
||
|
||
assert result is True
|
||
|
||
|
||
def test_overwrite_declined_false_when_no_history():
|
||
"""overwrite_skipped 为真但没有任何整理记录时,不应判定为保护场景。"""
|
||
task = make_task(1)
|
||
transferinfo = TransferInfo(success=False, overwrite_skipped=True)
|
||
transferhis = make_history_oper(history=None)
|
||
|
||
result = TransferChain._TransferChain__is_overwrite_declined(
|
||
task, transferinfo, transferhis
|
||
)
|
||
|
||
assert result is False
|
||
|
||
|
||
def test_overwrite_declined_false_when_only_failed_history():
|
||
"""overwrite_skipped 为真但只有失败记录时,不应判定为保护场景。"""
|
||
task = make_task(1)
|
||
failed_history = SimpleNamespace(id=2, status=False)
|
||
transferinfo = TransferInfo(success=False, overwrite_skipped=True)
|
||
transferhis = make_history_oper(history=failed_history, success_history=None)
|
||
|
||
result = TransferChain._TransferChain__is_overwrite_declined(
|
||
task, transferinfo, transferhis
|
||
)
|
||
|
||
assert result is False
|
||
|
||
|
||
def test_overwrite_declined_false_when_query_raises():
|
||
"""查询整理历史异常时应保守返回 False,不阻断原有失败语义。"""
|
||
task = make_task(1)
|
||
transferinfo = TransferInfo(success=False, overwrite_skipped=True)
|
||
transferhis = make_history_oper(raise_on_query=True)
|
||
|
||
result = TransferChain._TransferChain__is_overwrite_declined(
|
||
task, transferinfo, transferhis
|
||
)
|
||
|
||
assert result is False
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# __default_callback 失败分支
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _make_failed_task():
|
||
"""构造一个失败回调测试所需的最小整理任务。"""
|
||
task = make_task(1)
|
||
task.mediainfo = FakeMedia()
|
||
# __default_callback 失败通知路径需要读取海报图,FakeMedia 本身不提供该接口
|
||
task.mediainfo.get_message_image = lambda: "poster.jpg"
|
||
task.background = False
|
||
task.manual = True
|
||
return task
|
||
|
||
|
||
def test_default_callback_skips_history_and_notification_when_overwrite_declined():
|
||
"""
|
||
同源已有成功记录时,覆盖裁决不覆盖不应写失败历史、不应发送失败事件与通知。
|
||
"""
|
||
chain = make_transfer_chain()
|
||
chain.eventmanager = MagicMock()
|
||
chain.post_message = MagicMock()
|
||
|
||
task = _make_failed_task()
|
||
success_history = SimpleNamespace(id=99, status=True)
|
||
add_fail_calls = []
|
||
transfer_history_oper = make_history_oper(
|
||
history=success_history, add_fail_calls=add_fail_calls
|
||
)
|
||
|
||
transferinfo = TransferInfo(
|
||
success=False,
|
||
fileitem=task.fileitem,
|
||
message="目标已存在,按覆盖策略跳过覆盖",
|
||
transfer_type="copy",
|
||
overwrite_skipped=True,
|
||
need_notify=False,
|
||
)
|
||
|
||
with patch(
|
||
"app.chain.transfer.TransferHistoryOper",
|
||
return_value=transfer_history_oper,
|
||
), patch(
|
||
"app.chain.transfer.settings.AI_AGENT_ENABLE", False
|
||
), patch(
|
||
"app.chain.transfer.settings.AI_AGENT_RETRY_TRANSFER", False
|
||
):
|
||
state, errmsg = chain._TransferChain__default_callback(task, transferinfo)
|
||
|
||
assert state is False
|
||
assert errmsg == transferinfo.message
|
||
assert add_fail_calls == []
|
||
assert chain.post_message.call_count == 0
|
||
transfer_failed_events = [
|
||
call
|
||
for call in chain.eventmanager.send_event.call_args_list
|
||
if call.args[0] == EventType.TransferFailed
|
||
]
|
||
assert transfer_failed_events == []
|
||
|
||
|
||
def test_default_callback_keeps_original_failure_semantics_without_success_history():
|
||
"""
|
||
没有已有成功记录时(即使 overwrite_skipped 为真),仍应按原有语义写失败历史并通知。
|
||
"""
|
||
chain = make_transfer_chain()
|
||
chain.eventmanager = MagicMock()
|
||
chain.post_message = MagicMock()
|
||
|
||
task = _make_failed_task()
|
||
add_fail_calls = []
|
||
transfer_history_oper = make_history_oper(
|
||
history=None, add_fail_calls=add_fail_calls
|
||
)
|
||
|
||
transferinfo = TransferInfo(
|
||
success=False,
|
||
fileitem=task.fileitem,
|
||
message="目标已存在,按覆盖策略跳过覆盖",
|
||
transfer_type="copy",
|
||
overwrite_skipped=True,
|
||
need_notify=False,
|
||
)
|
||
|
||
with patch(
|
||
"app.chain.transfer.TransferHistoryOper",
|
||
return_value=transfer_history_oper,
|
||
), patch(
|
||
"app.chain.transfer.settings.AI_AGENT_ENABLE", False
|
||
), patch(
|
||
"app.chain.transfer.settings.AI_AGENT_RETRY_TRANSFER", False
|
||
):
|
||
state, errmsg = chain._TransferChain__default_callback(task, transferinfo)
|
||
|
||
assert state is False
|
||
assert errmsg == transferinfo.message
|
||
assert len(add_fail_calls) == 1
|
||
assert chain.post_message.call_count == 1
|
||
transfer_failed_events = [
|
||
call
|
||
for call in chain.eventmanager.send_event.call_args_list
|
||
if call.args[0] == EventType.TransferFailed
|
||
]
|
||
assert len(transfer_failed_events) == 1
|