Files
MoviePilot/tests/test_transfer_failure_notification_aggregation.py
T

168 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from unittest.mock import Mock
from app.chain import transfer as transfer_module
from app.chain.transfer import TransferChain
from app.application.transfer import (
TransferFailureNotification,
TransferFailureNotificationAggregator,
TransferTask,
build_transfer_failure_group_key,
)
from app.domain.context import MediaInfo
from app.domain.metainfo import MetaInfo
from app.runtime.config import ConfigModel
from app.schemas.file import FileItem
from app.schemas.transfer import TransferInfo
from app.schemas.types import MediaSource, MediaType
class _Timer:
"""记录静默窗口是否因新失败到达而取消。"""
def __init__(self, callback, args):
"""保存定时回调与参数。"""
self.callback = callback
self.args = args
self.cancelled = False
def cancel(self):
"""标记当前定时器已取消。"""
self.cancelled = True
class _Loop:
"""同步执行线程安全入队,并保留延迟回调供测试触发。"""
def __init__(self):
"""初始化定时器清单。"""
self.timers = []
def call_soon_threadsafe(self, callback, *args):
"""同步执行本应投递到事件循环的回调。"""
callback(*args)
def call_later(self, _delay, callback, *args):
"""保存延迟回调并返回可取消句柄。"""
timer = _Timer(callback, args)
self.timers.append(timer)
return timer
def _task(*, episode: int, download_hash: str = "hash-1") -> TransferTask:
"""构造同一媒体不同剧集的整理任务。"""
return TransferTask(
fileitem=FileItem(
storage="local",
path=f"/downloads/Show/Show.S01E{episode:02d}.mkv",
type="file",
name=f"Show.S01E{episode:02d}.mkv",
),
meta=MetaInfo(f"Show S01E{episode:02d}"),
mediainfo=MediaInfo(
media_source=MediaSource.TMDB,
media_id="100",
tmdb_id=100,
title="测试剧",
type=MediaType.TV,
year="2026",
),
download_hash=download_hash,
username="tester",
)
def test_failure_group_key_prefers_media_identity_and_season():
"""同一媒体同一季应跨文件共享分组键。"""
first = build_transfer_failure_group_key(_task(episode=1, download_hash="hash-a"))
second = build_transfer_failure_group_key(_task(episode=2, download_hash="hash-b"))
assert first == second
assert first == "media:themoviedb:100:season:1:user:tester"
def test_failure_notification_aggregation_defaults_on():
"""整理失败通知聚合默认开启。"""
field = ConfigModel.model_fields["TRANSFER_FAILURE_NOTIFICATION_AGGREGATION"]
assert field.default is True
def test_aggregator_debounces_same_group_and_flushes_once():
"""同组失败应重置定时器并一次性回调全部快照。"""
loop = _Loop()
aggregator = TransferFailureNotificationAggregator()
callback = Mock()
notices = [
TransferFailureNotification("测试剧 (2026)", "S01E01", "原因A", 1, None, "tester"),
TransferFailureNotification("测试剧 (2026)", "S01E02", "原因B", 2, None, "tester"),
]
for notice in notices:
aggregator.schedule(
group_key="media:test",
notification=notice,
callback=callback,
loop=loop,
)
assert loop.timers[0].cancelled is True
assert loop.timers[1].cancelled is False
loop.timers[1].callback(*loop.timers[1].args)
callback.assert_called_once_with(notices)
def test_aggregated_message_contains_count_reason_stats_and_batch_entry():
"""聚合消息应给出失败数、原因统计、历史 ID 和批量处理入口。"""
chain = object.__new__(TransferChain)
sent = []
chain.post_message = sent.append
notices = [
TransferFailureNotification("测试剧 (2026)", "S01E01", "未识别到媒体信息", 11, None, "tester"),
TransferFailureNotification("测试剧 (2026)", "S01E02", "目标已存在", 12, None, "tester"),
TransferFailureNotification("测试剧 (2026)", "S01E03", "目标已存在", 13, None, "tester"),
]
chain._send_transfer_failure_notifications(notices)
assert len(sent) == 1
message = sent[0]
assert message.title == "测试剧 (2026) 入库失败(3 个文件)"
assert "失败文件:3 个" in message.text
assert "- 目标已存在 × 2" in message.text
assert "整理记录:#11、#12、#13" in message.text
assert message.buttons == [[{
"text": "批量处理",
"url": transfer_module.settings.MP_DOMAIN("#/history"),
}]]
def test_enabled_queue_uses_shared_group_key(monkeypatch):
"""开启聚合后公开通知入口应投递到聚合器而不是立即发送。"""
chain = object.__new__(TransferChain)
chain.failure_notification_aggregator = Mock()
chain.post_message = Mock()
task = _task(episode=1)
transferinfo = TransferInfo(
success=False,
fileitem=task.fileitem,
message="整理失败",
transfer_type="copy",
)
loop = transfer_module.global_vars.loop
monkeypatch.setattr(
transfer_module.settings,
"TRANSFER_FAILURE_NOTIFICATION_AGGREGATION",
True,
)
chain.queue_failed_transfer_notification(
task=task,
transferinfo=transferinfo,
history_id=22,
)
chain.failure_notification_aggregator.schedule.assert_called_once()
kwargs = chain.failure_notification_aggregator.schedule.call_args.kwargs
assert kwargs["group_key"] == build_transfer_failure_group_key(task)
assert kwargs["loop"] is loop
chain.post_message.assert_not_called()