Files
MoviePilot/tests/test_transfer_failed_retry_buttons.py
T

342 lines
13 KiB
Python

import unittest
import asyncio
import sys
from dataclasses import replace
from types import ModuleType
from types import SimpleNamespace
from unittest.mock import Mock, patch
sys.modules.setdefault("qbittorrentapi", ModuleType("qbittorrentapi"))
setattr(sys.modules["qbittorrentapi"], "TorrentFilesList", list)
sys.modules.setdefault("transmission_rpc", ModuleType("transmission_rpc"))
setattr(sys.modules["transmission_rpc"], "File", object)
sys.modules.setdefault("psutil", ModuleType("psutil"))
from app.chain.message import MessageChain
from app.chain.transfer import TransferChain
from app.application.messaging.interaction import InteractionContext
from app.runtime.config import global_vars, settings
from app.runtime.tasks import TaskRegistry
from app.schemas.types import NotificationChannel
class TestTransferFailedRetryButtons(unittest.TestCase):
def test_build_failed_transfer_buttons(self):
buttons = TransferChain.build_failed_transfer_buttons(12)
self.assertEqual(
buttons,
[
[
{"text": "重试", "callback_data": "transfer_retry_12"},
{
"text": "智能助手接管",
"callback_data": "transfer_ai_retry_12",
},
]
],
)
def test_remote_transfer_supports_history_only_retry(self):
chain = TransferChain()
with patch.object(chain, "redo_transfer_history", return_value=(True, "")) as redo:
with patch.object(chain, "post_message") as post_message:
chain.remote_transfer(
"12",
channel=NotificationChannel.Telegram,
userid="10001",
source="telegram-test",
)
redo.assert_called_once_with(12)
post_message.assert_not_called()
def test_message_chain_routes_transfer_callback_to_transfer_chain(self):
"""MessageChain 收到整理失败按钮回调时委托 TransferChain 处理。"""
chain = MessageChain()
with patch("app.chain.message.TransferChain") as transfer_cls:
transfer_cls.return_value.handle_failed_transfer_callback.return_value = True
chain._handle_callback(
callback_data="transfer_retry_12",
context=InteractionContext(
channel=NotificationChannel.Telegram,
source="telegram-test",
user_id="10001",
username="tester",
),
)
transfer_cls.return_value.handle_failed_transfer_callback.assert_called_once_with(
callback_data="transfer_retry_12",
channel=NotificationChannel.Telegram,
source="telegram-test",
userid="10001",
username="tester",
)
def test_transfer_retry_callback_retries_history(self):
chain = TransferChain()
with patch.object(chain, "redo_transfer_history", return_value=(True, "")) as redo:
with patch.object(chain, "post_message") as post_message:
handled = chain.handle_failed_transfer_callback(
callback_data="transfer_retry_12",
channel=NotificationChannel.Telegram,
source="telegram-test",
userid="10001",
username="tester",
)
self.assertTrue(handled)
redo.assert_called_once_with(12)
self.assertEqual(post_message.call_count, 2)
self.assertEqual(
post_message.call_args_list[0].args[0].title,
"开始重新整理记录 #12 ...",
)
self.assertEqual(
post_message.call_args_list[1].args[0].title,
"整理记录 #12 已重新整理",
)
def test_transfer_ai_retry_callback_schedules_agent_takeover(self):
chain = TransferChain()
chain.runtime_config = replace(chain.runtime_config, ai_agent_enable=True)
history = SimpleNamespace(
id=34,
status=False,
title="Test Show",
type="电视剧",
category=None,
year="2024",
seasons="S01",
episodes="E01",
src="/downloads/Test.Show.S01E01.mkv",
src_storage="local",
src_fileitem={"path": "/downloads/Test.Show.S01E01.mkv"},
dest=None,
dest_storage=None,
dest_fileitem=None,
mode="copy",
tmdbid=123,
doubanid=None,
bangumiid=None,
anilistid=None,
media_source="themoviedb",
media_id="123",
errmsg="未识别到媒体信息",
)
async_messages = []
def _run_pending_coro(coro, *args, **kwargs):
asyncio.run(coro)
async def _capture_message(message):
async_messages.append(message)
async def _finish_immediately(**kwargs):
kwargs["output_callback"]("ok")
manager = SimpleNamespace(run_background_prompt=_finish_immediately)
loop = Mock(**{"is_running.return_value": True, "is_closed.return_value": False})
with patch.object(global_vars, "CURRENT_EVENT_LOOP", loop), patch.object(
settings, "AI_AGENT_ENABLE", True
):
with patch(
"app.chain._transfer.get_chain_transfer_history_port"
) as history_oper_cls, patch(
"app.chain._transfer.build_manual_redo_prompt",
return_value="retry transfer prompt",
), patch(
"app.chain._transfer.get_running_agent_manager", return_value=manager
), patch("app.chain._transfer.get_task_registry") as get_registry:
get_registry.return_value.submit_threadsafe.side_effect = (
_run_pending_coro
)
history_oper_cls.return_value.get.return_value = history
with patch.object(chain, "async_post_message", side_effect=_capture_message):
chain.handle_failed_transfer_callback(
callback_data="transfer_ai_retry_34",
channel=NotificationChannel.Telegram,
source="telegram-test",
userid="10001",
username="tester",
)
get_registry.return_value.submit_threadsafe.assert_called_once()
self.assertEqual(
get_registry.return_value.submit_threadsafe.call_args.kwargs["owner"],
"chain.transfer.ai_takeover",
)
self.assertEqual(len(async_messages), 2)
self.assertEqual(
async_messages[0].title,
"已将整理记录 #34 交给智能助手处理",
)
self.assertEqual(async_messages[1].title, "智能助手整理完成")
def test_transfer_ai_retry_callback_reports_closed_task_registry(self):
"""宿主停止接收任务时,不得向用户报告智能助手已接管。"""
chain = TransferChain()
chain.runtime_config = replace(chain.runtime_config, ai_agent_enable=True)
history = SimpleNamespace(id=34)
registry = TaskRegistry()
asyncio.run(registry.shutdown(timeout_seconds=0.01))
loop = Mock(**{"is_running.return_value": True, "is_closed.return_value": False})
with patch.object(global_vars, "CURRENT_EVENT_LOOP", loop), patch.object(
settings, "AI_AGENT_ENABLE", True
), patch(
"app.chain._transfer.get_chain_transfer_history_port"
) as history_port, patch(
"app.chain._transfer.build_manual_redo_prompt",
return_value="retry transfer prompt",
), patch(
"app.chain._transfer.get_task_registry", return_value=registry
), patch(
"app.chain._transfer.logger"
) as logger, patch.object(
chain, "post_message"
) as post_message:
history_port.return_value.get.return_value = history
chain.handle_failed_transfer_callback(
callback_data="transfer_ai_retry_34",
channel=NotificationChannel.Telegram,
source="telegram-test",
userid="10001",
username="tester",
)
logger.warning.assert_called_once()
self.assertEqual(post_message.call_count, 1)
self.assertEqual(
post_message.call_args.args[0].title,
"智能助手整理失败",
)
self.assertNotIn("已将", post_message.call_args.args[0].title)
def test_transfer_ai_retry_callback_reports_unavailable_event_loop(self):
"""主循环不可用时,应在创建后台协程前返回明确失败提示。"""
chain = TransferChain()
chain.runtime_config = replace(chain.runtime_config, ai_agent_enable=True)
history = SimpleNamespace(id=34)
with patch.object(global_vars, "CURRENT_EVENT_LOOP", None), patch.object(
settings, "AI_AGENT_ENABLE", True
), patch(
"app.chain._transfer.get_chain_transfer_history_port"
) as history_port, patch(
"app.chain._transfer.build_manual_redo_prompt",
return_value="retry transfer prompt",
), patch(
"app.chain._transfer.get_task_registry"
) as get_registry, patch(
"app.chain._transfer.logger"
) as logger, patch.object(
chain, "post_message"
) as post_message:
history_port.return_value.get.return_value = history
chain.handle_failed_transfer_callback(
callback_data="transfer_ai_retry_34",
channel=NotificationChannel.Telegram,
source="telegram-test",
userid="10001",
username="tester",
)
get_registry.return_value.submit_threadsafe.assert_not_called()
logger.warning.assert_called_once()
self.assertEqual(post_message.call_count, 1)
self.assertEqual(
post_message.call_args.args[0].title,
"智能助手整理失败",
)
def test_transfer_ai_retry_callback_uses_successful_move_dest_as_source(self):
chain = TransferChain()
chain.runtime_config = replace(chain.runtime_config, ai_agent_enable=True)
captured = {}
history = SimpleNamespace(
id=35,
status=True,
title="Test Show",
type="电视剧",
category=None,
year="2024",
seasons="S01",
episodes="E01",
src="/downloads/Test.Show.S01E01.mkv",
src_storage="local",
src_fileitem={"path": "/downloads/Test.Show.S01E01.mkv"},
dest="/library/Test Show (2024)/Season 1/Test.Show.S01E01.mkv",
dest_storage="local",
dest_fileitem={
"storage": "local",
"path": "/library/Test Show (2024)/Season 1/Test.Show.S01E01.mkv",
"name": "Test.Show.S01E01.mkv",
"type": "file",
},
mode="move",
tmdbid=123,
doubanid=None,
bangumiid=None,
anilistid=None,
media_source="themoviedb",
media_id="123",
errmsg=None,
)
def _run_pending_coro(coro, *args, **kwargs):
asyncio.run(coro)
return SimpleNamespace()
async def fake_run_background_prompt(**kwargs):
captured["message"] = kwargs["message"]
output_callback = kwargs.get("output_callback")
if output_callback:
output_callback("ok")
async def fake_async_post_message(*args, **kwargs):
return None
from app.agent.prompt.transfer_redo import build_manual_redo_prompt
manager = SimpleNamespace(run_background_prompt=fake_run_background_prompt)
loop = Mock(**{"is_running.return_value": True, "is_closed.return_value": False})
with patch.object(global_vars, "CURRENT_EVENT_LOOP", loop), patch.object(
settings, "AI_AGENT_ENABLE", True
):
with patch(
"app.chain._transfer.get_chain_transfer_history_port"
) as history_oper_cls, patch(
"app.chain._transfer.build_manual_redo_prompt",
side_effect=build_manual_redo_prompt,
), patch(
"app.chain._transfer.get_running_agent_manager",
return_value=manager,
), patch("app.chain._transfer.get_task_registry") as get_registry:
get_registry.return_value.submit_threadsafe.side_effect = (
_run_pending_coro
)
history_oper_cls.return_value.get.return_value = history
with patch.object(chain, "post_message"), patch.object(
chain, "async_post_message", side_effect=fake_async_post_message
):
chain.handle_failed_transfer_callback(
callback_data="transfer_ai_retry_35",
channel=NotificationChannel.Telegram,
source="telegram-test",
userid="10001",
username="tester",
)
self.assertIn(
"- Source path: /library/Test Show (2024)/Season 1/Test.Show.S01E01.mkv",
captured["message"],
)