mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-21 15:36:37 +08:00
fix downloader task status queries
This commit is contained in:
199
tests/test_agent_query_download_tasks_tool.py
Normal file
199
tests/test_agent_query_download_tasks_tool.py
Normal file
@@ -0,0 +1,199 @@
|
||||
import asyncio
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from app.agent.tools.impl.query_download_tasks import QueryDownloadTasksTool
|
||||
from app.schemas import DownloaderTorrent
|
||||
|
||||
|
||||
def test_completed_status_returns_qbittorrent_and_transmission_completed_states():
|
||||
"""
|
||||
按完成状态查询时应包含 QB/TR 中非下载中、非暂停的实际状态。
|
||||
"""
|
||||
completed_torrents = [
|
||||
DownloaderTorrent(
|
||||
downloader="qb",
|
||||
hash="hash-qb",
|
||||
title="QB Done",
|
||||
size=1024,
|
||||
progress=100,
|
||||
state="completed",
|
||||
tags="moviepilot",
|
||||
),
|
||||
DownloaderTorrent(
|
||||
downloader="tr",
|
||||
hash="hash-tr",
|
||||
title="TR Done",
|
||||
size=2048,
|
||||
progress=100,
|
||||
state="completed",
|
||||
tags="moviepilot",
|
||||
),
|
||||
]
|
||||
download_chain = MagicMock()
|
||||
download_chain.list_torrents.return_value = completed_torrents
|
||||
|
||||
with patch(
|
||||
"app.agent.tools.impl.query_download_tasks.DownloadChain",
|
||||
return_value=download_chain,
|
||||
), patch.object(
|
||||
QueryDownloadTasksTool,
|
||||
"_load_history_map",
|
||||
return_value={},
|
||||
):
|
||||
result = QueryDownloadTasksTool._query_downloads_sync(status="completed")
|
||||
|
||||
assert result["downloads"] == completed_torrents
|
||||
download_chain.list_torrents.assert_called_once_with(
|
||||
downloader=None,
|
||||
status="completed",
|
||||
include_all_tags=False,
|
||||
)
|
||||
|
||||
|
||||
def test_run_completed_status_formats_completed_download_tasks():
|
||||
"""
|
||||
工具输出应保留完成任务的实际下载器状态,便于用户判断来源。
|
||||
"""
|
||||
completed_torrents = [
|
||||
DownloaderTorrent(
|
||||
downloader="qb",
|
||||
hash="hash-qb",
|
||||
title="QB Done",
|
||||
size=1024,
|
||||
progress=100,
|
||||
state="completed",
|
||||
tags="moviepilot",
|
||||
)
|
||||
]
|
||||
|
||||
with patch.object(
|
||||
QueryDownloadTasksTool,
|
||||
"_query_downloads_sync",
|
||||
return_value={"downloads": completed_torrents},
|
||||
):
|
||||
result = asyncio.run(
|
||||
QueryDownloadTasksTool(session_id="session-1", user_id="10001").run(
|
||||
status="completed"
|
||||
)
|
||||
)
|
||||
|
||||
payload = json.loads(result)
|
||||
assert payload[0]["hash"] == "hash-qb"
|
||||
assert payload[0]["state"] == "completed"
|
||||
|
||||
|
||||
def test_include_all_tags_passes_scope_to_downloader_query():
|
||||
"""
|
||||
智能体显式扩大范围时,应查询未打 MoviePilot 内置标签的下载任务。
|
||||
"""
|
||||
all_scope_torrents = [
|
||||
DownloaderTorrent(
|
||||
downloader="qb",
|
||||
hash="hash-external",
|
||||
title="External Task",
|
||||
size=1024,
|
||||
progress=10,
|
||||
state="downloading",
|
||||
tags="external",
|
||||
)
|
||||
]
|
||||
download_chain = MagicMock()
|
||||
download_chain.list_torrents.return_value = all_scope_torrents
|
||||
|
||||
with patch(
|
||||
"app.agent.tools.impl.query_download_tasks.DownloadChain",
|
||||
return_value=download_chain,
|
||||
), patch.object(
|
||||
QueryDownloadTasksTool,
|
||||
"_load_history_map",
|
||||
return_value={},
|
||||
):
|
||||
result = QueryDownloadTasksTool._query_downloads_sync(
|
||||
status="all",
|
||||
include_all_tags=True,
|
||||
)
|
||||
|
||||
assert result["downloads"] == all_scope_torrents
|
||||
download_chain.list_torrents.assert_called_once_with(
|
||||
downloader=None,
|
||||
status=None,
|
||||
include_all_tags=True,
|
||||
)
|
||||
|
||||
|
||||
def test_include_all_tags_downloading_status_uses_list_torrents():
|
||||
"""
|
||||
查询全部标签范围的下载中任务时,不应走只面向 MoviePilot 任务的便捷方法。
|
||||
"""
|
||||
download_chain = MagicMock()
|
||||
download_chain.list_torrents.return_value = [
|
||||
DownloaderTorrent(
|
||||
downloader="tr",
|
||||
hash="hash-downloading",
|
||||
title="Downloading External",
|
||||
size=2048,
|
||||
progress=50,
|
||||
state="downloading",
|
||||
tags="external",
|
||||
)
|
||||
]
|
||||
|
||||
with patch(
|
||||
"app.agent.tools.impl.query_download_tasks.DownloadChain",
|
||||
return_value=download_chain,
|
||||
), patch.object(
|
||||
QueryDownloadTasksTool,
|
||||
"_load_history_map",
|
||||
return_value={},
|
||||
):
|
||||
result = QueryDownloadTasksTool._query_downloads_sync(
|
||||
status="downloading",
|
||||
include_all_tags=True,
|
||||
)
|
||||
|
||||
assert result["downloads"][0].hash == "hash-downloading"
|
||||
download_chain.downloading.assert_not_called()
|
||||
download_chain.list_torrents.assert_called_once_with(
|
||||
downloader=None,
|
||||
status="downloading",
|
||||
include_all_tags=True,
|
||||
)
|
||||
|
||||
|
||||
def test_include_all_tags_false_string_keeps_builtin_tag_scope():
|
||||
"""
|
||||
CLI 字符串 false 不应被 Python 真值规则误判为扩大查询范围。
|
||||
"""
|
||||
download_chain = MagicMock()
|
||||
download_chain.list_torrents.return_value = [
|
||||
DownloaderTorrent(
|
||||
downloader="qb",
|
||||
hash="hash-moviepilot",
|
||||
title="MoviePilot Task",
|
||||
size=1024,
|
||||
progress=100,
|
||||
state="completed",
|
||||
tags="moviepilot",
|
||||
)
|
||||
]
|
||||
|
||||
with patch(
|
||||
"app.agent.tools.impl.query_download_tasks.DownloadChain",
|
||||
return_value=download_chain,
|
||||
), patch.object(
|
||||
QueryDownloadTasksTool,
|
||||
"_load_history_map",
|
||||
return_value={},
|
||||
):
|
||||
result = QueryDownloadTasksTool._query_downloads_sync(
|
||||
status="completed",
|
||||
include_all_tags="false",
|
||||
)
|
||||
|
||||
assert result["downloads"][0].hash == "hash-moviepilot"
|
||||
download_chain.list_torrents.assert_called_once_with(
|
||||
downloader=None,
|
||||
status="completed",
|
||||
include_all_tags=False,
|
||||
)
|
||||
@@ -133,10 +133,26 @@ def _load_transmission_module():
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
class _DownloaderTorrent:
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
class TorrentStatus(Enum):
|
||||
TRANSFER = "transfer"
|
||||
DOWNLOADING = "downloading"
|
||||
|
||||
class TorrentQueryStatus(Enum):
|
||||
ALL = "all"
|
||||
TRANSFER = "transfer"
|
||||
DOWNLOADING = "downloading"
|
||||
COMPLETED = "completed"
|
||||
PAUSED = "paused"
|
||||
|
||||
class DownloadTaskState(Enum):
|
||||
DOWNLOADING = "downloading"
|
||||
PAUSED = "paused"
|
||||
COMPLETED = "completed"
|
||||
|
||||
class _Logger:
|
||||
def debug(self, *_args, **_kwargs):
|
||||
pass
|
||||
@@ -179,8 +195,11 @@ def _load_transmission_module():
|
||||
cache_module.FileCache = _FileCache
|
||||
schemas_module.TransferTorrent = _TransferTorrent
|
||||
schemas_module.DownloadingTorrent = _DownloadingTorrent
|
||||
schemas_module.DownloaderTorrent = _DownloaderTorrent
|
||||
schemas_module.DownloaderInfo = object
|
||||
schema_types_module.TorrentStatus = TorrentStatus
|
||||
schema_types_module.TorrentQueryStatus = TorrentQueryStatus
|
||||
schema_types_module.DownloadTaskState = DownloadTaskState
|
||||
schema_types_module.ModuleType = Enum("ModuleType", {"Downloader": "downloader"})
|
||||
schema_types_module.DownloaderType = Enum(
|
||||
"DownloaderType", {"Transmission": "Transmission"}
|
||||
@@ -359,3 +378,62 @@ class TransmissionPathMappingTest(unittest.TestCase):
|
||||
Path("/mnt/raid5/home_lt999lt/video/downloads/movie/Movie"),
|
||||
"tr",
|
||||
)
|
||||
|
||||
def test_all_torrents_include_completed_and_downloading_states(self):
|
||||
server = MagicMock()
|
||||
server.get_torrents.return_value = (
|
||||
[
|
||||
SimpleNamespace(
|
||||
name="Completed",
|
||||
download_dir="/mnt/raid5/home_lt999lt/video/downloads/movie",
|
||||
hashString="hash-completed",
|
||||
total_size=1024,
|
||||
labels=[],
|
||||
progress=100,
|
||||
status="seed_pending",
|
||||
),
|
||||
SimpleNamespace(
|
||||
name="Downloading",
|
||||
download_dir="/mnt/raid5/home_lt999lt/video/downloads/movie",
|
||||
hashString="hash-downloading",
|
||||
total_size=2048,
|
||||
labels=[],
|
||||
progress=50,
|
||||
status="downloading",
|
||||
rate_download=1024,
|
||||
rate_upload=0,
|
||||
left_until_done=1024,
|
||||
),
|
||||
],
|
||||
False,
|
||||
)
|
||||
module = self._build_module(server)
|
||||
|
||||
torrents = module.list_torrents()
|
||||
|
||||
self.assertEqual(["completed", "downloading"], [torrent.state for torrent in torrents])
|
||||
self.assertEqual(["hash-completed", "hash-downloading"], [torrent.hash for torrent in torrents])
|
||||
server.get_torrents.assert_called_once_with(tags="moviepilot-tag")
|
||||
|
||||
def test_include_all_tags_removes_builtin_tag_filter(self):
|
||||
server = MagicMock()
|
||||
server.get_torrents.return_value = (
|
||||
[
|
||||
SimpleNamespace(
|
||||
name="External",
|
||||
download_dir="/mnt/raid5/home_lt999lt/video/downloads/movie",
|
||||
hashString="hash-external",
|
||||
total_size=1024,
|
||||
labels=["external"],
|
||||
progress=100,
|
||||
status="seeding",
|
||||
)
|
||||
],
|
||||
False,
|
||||
)
|
||||
module = self._build_module(server)
|
||||
|
||||
torrents = module.list_torrents(include_all_tags=True)
|
||||
|
||||
self.assertEqual(["hash-external"], [torrent.hash for torrent in torrents])
|
||||
server.get_torrents.assert_called_once_with(tags=None)
|
||||
|
||||
@@ -92,10 +92,26 @@ def _load_qbittorrent_modules():
|
||||
def from_string(content):
|
||||
return types.SimpleNamespace(name="test", total_size=len(content))
|
||||
|
||||
class _DownloaderTorrent:
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
class TorrentStatus(Enum):
|
||||
TRANSFER = "transfer"
|
||||
DOWNLOADING = "downloading"
|
||||
|
||||
class TorrentQueryStatus(Enum):
|
||||
ALL = "all"
|
||||
TRANSFER = "transfer"
|
||||
DOWNLOADING = "downloading"
|
||||
COMPLETED = "completed"
|
||||
PAUSED = "paused"
|
||||
|
||||
class DownloadTaskState(Enum):
|
||||
DOWNLOADING = "downloading"
|
||||
PAUSED = "paused"
|
||||
COMPLETED = "completed"
|
||||
|
||||
class ModuleType(Enum):
|
||||
Downloader = "Downloader"
|
||||
|
||||
@@ -109,7 +125,10 @@ def _load_qbittorrent_modules():
|
||||
schemas_module.DownloaderInfo = object
|
||||
schemas_module.TransferTorrent = object
|
||||
schemas_module.DownloadingTorrent = object
|
||||
schemas_module.DownloaderTorrent = _DownloaderTorrent
|
||||
schema_types_module.TorrentStatus = TorrentStatus
|
||||
schema_types_module.TorrentQueryStatus = TorrentQueryStatus
|
||||
schema_types_module.DownloadTaskState = DownloadTaskState
|
||||
schema_types_module.ModuleType = ModuleType
|
||||
schema_types_module.DownloaderType = DownloaderType
|
||||
string_module.StringUtils = _StringUtils
|
||||
@@ -253,6 +272,83 @@ def test_login_skips_incomplete_file_suffix_when_already_matches():
|
||||
fake_client.app_set_preferences.assert_not_called()
|
||||
|
||||
|
||||
def test_completed_status_includes_qbittorrent_finished_upload_states():
|
||||
"""
|
||||
qBittorrent 按完成状态查询时应包含非下载中、非暂停的上传侧状态。
|
||||
"""
|
||||
server = MagicMock()
|
||||
server.get_torrents.return_value = (
|
||||
[
|
||||
{
|
||||
"name": "QB Done",
|
||||
"content_path": "/downloads/QB Done",
|
||||
"hash": "hash-qb",
|
||||
"total_size": 1024,
|
||||
"completed": 1024,
|
||||
"progress": 1,
|
||||
"state": "stalledUP",
|
||||
"tags": "moviepilot-tag",
|
||||
"dlspeed": 0,
|
||||
"upspeed": 128,
|
||||
},
|
||||
{
|
||||
"name": "QB Downloading",
|
||||
"content_path": "/downloads/QB Downloading",
|
||||
"hash": "hash-downloading",
|
||||
"total_size": 2048,
|
||||
"completed": 1024,
|
||||
"progress": 0.5,
|
||||
"state": "queuedDL",
|
||||
"tags": "moviepilot-tag",
|
||||
"dlspeed": 64,
|
||||
"upspeed": 0,
|
||||
},
|
||||
],
|
||||
False,
|
||||
)
|
||||
module = QbittorrentModule.__new__(QbittorrentModule)
|
||||
module.get_instances = MagicMock(return_value={"qb": server})
|
||||
module.normalize_return_path = MagicMock(side_effect=lambda path, _name: str(path))
|
||||
|
||||
torrents = module.list_torrents(status="completed")
|
||||
|
||||
assert [torrent.hash for torrent in torrents] == ["hash-qb"]
|
||||
assert torrents[0].state == "completed"
|
||||
server.get_torrents.assert_called_once_with(tags="moviepilot-tag")
|
||||
|
||||
|
||||
def test_list_torrents_include_all_tags_removes_builtin_tag_filter():
|
||||
"""
|
||||
智能体扩大查询范围时,qBittorrent 查询应取消内置标签过滤。
|
||||
"""
|
||||
server = MagicMock()
|
||||
server.get_torrents.return_value = (
|
||||
[
|
||||
{
|
||||
"name": "External Task",
|
||||
"content_path": "/downloads/External Task",
|
||||
"hash": "hash-external",
|
||||
"total_size": 1024,
|
||||
"completed": 1024,
|
||||
"progress": 1,
|
||||
"state": "stalledUP",
|
||||
"tags": "external",
|
||||
"dlspeed": 0,
|
||||
"upspeed": 0,
|
||||
}
|
||||
],
|
||||
False,
|
||||
)
|
||||
module = QbittorrentModule.__new__(QbittorrentModule)
|
||||
module.get_instances = MagicMock(return_value={"qb": server})
|
||||
module.normalize_return_path = MagicMock(side_effect=lambda path, _name: str(path))
|
||||
|
||||
torrents = module.list_torrents(include_all_tags=True)
|
||||
|
||||
assert [torrent.hash for torrent in torrents] == ["hash-external"]
|
||||
server.get_torrents.assert_called_once_with(tags=None)
|
||||
|
||||
|
||||
def test_add_torrent_accepts_structured_success_response():
|
||||
"""新版 qBittorrent API 结构化成功响应应返回新增种子 ID。"""
|
||||
fake_client = MagicMock()
|
||||
|
||||
Reference in New Issue
Block a user