import unittest import asyncio import sys from types import ModuleType from types import SimpleNamespace from unittest.mock import 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.core.config import settings from app.schemas.types import MessageChannel 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=MessageChannel.Telegram, userid="10001", source="telegram-test", ) redo.assert_called_once_with(12) post_message.assert_not_called() def test_transfer_retry_callback_retries_history(self): chain = MessageChain() with patch("app.chain.message.TransferChain") as transfer_cls: transfer_cls.return_value.redo_transfer_history.return_value = (True, "") with patch.object(chain, "post_message") as post_message: chain._handle_callback( text="CALLBACK:transfer_retry_12", channel=MessageChannel.Telegram, source="telegram-test", userid="10001", username="tester", ) transfer_cls.return_value.redo_transfer_history.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 = MessageChain() 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, errmsg="未识别到媒体信息", ) def _close_pending_coro(coro, *args, **kwargs): """关闭被调度的协程:测试中事件循环未运行,不关闭会残留 never-awaited 警告。""" coro.close() with patch.object(settings, "AI_AGENT_ENABLE", True): with patch( "app.chain.message.TransferHistoryOper" ) as history_oper_cls, patch( "app.chain.message.asyncio.run_coroutine_threadsafe", side_effect=_close_pending_coro, ) as run_task: history_oper_cls.return_value.get.return_value = history with patch.object(chain, "post_message") as post_message: chain._handle_callback( text="CALLBACK:transfer_ai_retry_34", channel=MessageChannel.Telegram, source="telegram-test", userid="10001", username="tester", ) run_task.assert_called_once() self.assertEqual(post_message.call_count, 1) self.assertEqual( post_message.call_args_list[0].args[0].title, "已将整理记录 #34 交给智能助手处理", ) def test_transfer_ai_retry_callback_uses_successful_move_dest_as_source(self): chain = MessageChain() 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, 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 with patch.object(settings, "AI_AGENT_ENABLE", True): with patch( "app.chain.message.TransferHistoryOper" ) as history_oper_cls, patch( "app.chain.message.agent_manager.run_background_prompt", side_effect=fake_run_background_prompt, ), patch( "app.chain.message.asyncio.run_coroutine_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_callback( text="CALLBACK:transfer_ai_retry_35", channel=MessageChannel.Telegram, source="telegram-test", userid="10001", username="tester", ) self.assertIn( "- Source path: /library/Test Show (2024)/Season 1/Test.Show.S01E01.mkv", captured["message"], )