mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-17 04:03:57 +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>
694 lines
20 KiB
Python
694 lines
20 KiB
Python
from types import SimpleNamespace
|
||
|
||
from app.api.endpoints.transfer import (
|
||
manual_transfer as manual_transfer_endpoint,
|
||
query_manual_transfer_history,
|
||
)
|
||
from app.chain.transfer import TransferChain
|
||
from app.core.config import settings
|
||
from app.db.transferhistory_oper import TransferHistoryOper
|
||
from app.helper.transferhistory import (
|
||
clear_transfer_failures,
|
||
failed_retry_count,
|
||
max_failed_retries,
|
||
record_transfer_failure,
|
||
)
|
||
from app.schemas import FileItem, ManualTransferItem
|
||
from tests.test_transfer_sync_extra_files import (
|
||
FakeMeta,
|
||
make_fileitem,
|
||
make_transfer_chain,
|
||
)
|
||
|
||
|
||
def _reset_failed_retries(src_path, storage=None):
|
||
"""清空失败重试计数,隔离用例之间共享的模块级计数缓存。"""
|
||
clear_transfer_failures(src_path, storage)
|
||
|
||
|
||
def _patch_transfer_planning(monkeypatch, chain, fileitem, history, planned, deleted):
|
||
"""隔离整理规划依赖,并记录历史及旧目标清理动作。"""
|
||
monkeypatch.setattr(
|
||
chain,
|
||
"_TransferChain__get_trans_fileitems",
|
||
lambda current_fileitem, predicate: [(fileitem, False)],
|
||
)
|
||
monkeypatch.setattr(chain, "_TransferChain__put_to_jobview", lambda task: True)
|
||
monkeypatch.setattr(
|
||
chain,
|
||
"_TransferChain__register_scrape_batch_task",
|
||
lambda task: None,
|
||
)
|
||
monkeypatch.setattr(
|
||
chain,
|
||
"_TransferChain__close_scrape_batch",
|
||
lambda batch_id: None,
|
||
)
|
||
|
||
def fake_handle_transfer(task, callback=None):
|
||
"""记录进入实际整理阶段的文件。"""
|
||
planned.append(task.fileitem.path)
|
||
return True, ""
|
||
|
||
monkeypatch.setattr(chain, "_TransferChain__handle_transfer", fake_handle_transfer)
|
||
|
||
history_oper = SimpleNamespace(
|
||
get_by_src=lambda src, storage=None: history,
|
||
get_success_by_src=lambda src, storage=None: history if history and history.status else None,
|
||
get_by_dest=lambda dest, storage=None: None,
|
||
delete=lambda history_id: deleted.append(("history", history_id)),
|
||
)
|
||
monkeypatch.setattr(
|
||
"app.chain.transfer.TransferHistoryOper",
|
||
lambda: history_oper,
|
||
)
|
||
monkeypatch.setattr(
|
||
"app.chain.transfer.DownloadHistoryOper",
|
||
lambda: SimpleNamespace(
|
||
get_by_hash=lambda download_hash: None,
|
||
get_file_by_fullpath=lambda fullpath: None,
|
||
get_files_by_savepath=lambda savepath: [],
|
||
get_by_path=lambda path: None,
|
||
),
|
||
)
|
||
monkeypatch.setattr(
|
||
"app.chain.transfer.SystemConfigOper",
|
||
lambda: SimpleNamespace(get=lambda key: None),
|
||
)
|
||
monkeypatch.setattr(
|
||
"app.chain.transfer.StorageChain",
|
||
lambda: SimpleNamespace(
|
||
exists=lambda current_fileitem: True,
|
||
delete_media_file=lambda current_fileitem: deleted.append(
|
||
("target", current_fileitem.path)
|
||
)
|
||
or True,
|
||
),
|
||
)
|
||
monkeypatch.setattr(
|
||
"app.chain.transfer.MetaInfoPath",
|
||
lambda path, custom_words=None, **kwargs: FakeMeta(1),
|
||
)
|
||
|
||
|
||
def test_query_manual_transfer_history_returns_success_summary(monkeypatch):
|
||
"""手动整理初始化接口应只返回成功历史摘要供前端切换重整状态。"""
|
||
fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
|
||
captured = []
|
||
|
||
class _FakeTransferChain:
|
||
"""记录历史查询收到的文件项。"""
|
||
|
||
def get_manual_transfer_histories(self, fileitems):
|
||
"""返回两条成功历史记录。"""
|
||
captured.extend(fileitems)
|
||
return [SimpleNamespace(id=1), SimpleNamespace(id=2)]
|
||
|
||
monkeypatch.setattr(
|
||
"app.api.endpoints.transfer.TransferChain",
|
||
_FakeTransferChain,
|
||
)
|
||
|
||
response = query_manual_transfer_history(
|
||
transer_item=ManualTransferItem(fileitem=fileitem),
|
||
db=object(),
|
||
_="token",
|
||
)
|
||
|
||
assert response.success is True
|
||
assert response.data == {"reorganize": True, "history_count": 2}
|
||
assert captured == [fileitem]
|
||
|
||
|
||
def test_manual_transfer_endpoint_passes_reorganize_confirmation(monkeypatch):
|
||
"""手动整理接口应把前端重整确认传入整理链。"""
|
||
fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
|
||
captured = {}
|
||
|
||
class _FakeTransferChain:
|
||
"""记录手动整理调用参数。"""
|
||
|
||
def manual_transfer(self, **kwargs):
|
||
"""保存整理参数并返回成功。"""
|
||
captured.update(kwargs)
|
||
return True, ""
|
||
|
||
monkeypatch.setattr(
|
||
"app.api.endpoints.transfer.TransferChain",
|
||
_FakeTransferChain,
|
||
)
|
||
|
||
response = manual_transfer_endpoint(
|
||
transer_item=ManualTransferItem(
|
||
fileitem=fileitem,
|
||
reorganize=True,
|
||
),
|
||
background=False,
|
||
db=object(),
|
||
_="token",
|
||
)
|
||
|
||
assert response.success is True
|
||
assert captured["reorganize"] is True
|
||
|
||
|
||
def test_history_endpoint_reorganize_uses_chain_cleanup(monkeypatch):
|
||
"""历史 ID 重整应交由整理链清理历史,不能再传入旧目标重复删除。"""
|
||
src_fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
|
||
dest_fileitem = make_fileitem("/library/Test Show/Test.Show.S01E01.mkv")
|
||
history = SimpleNamespace(
|
||
id=21,
|
||
status=True,
|
||
mode="copy",
|
||
src_fileitem=src_fileitem.model_dump(),
|
||
dest_fileitem=dest_fileitem.model_dump(),
|
||
)
|
||
captured = {}
|
||
|
||
class _FakeTransferChain:
|
||
"""记录历史 ID 重整调用参数。"""
|
||
|
||
def manual_transfer(self, **kwargs):
|
||
"""保存整理参数并返回成功。"""
|
||
captured.update(kwargs)
|
||
return True, ""
|
||
|
||
monkeypatch.setattr(
|
||
"app.api.endpoints.transfer.TransferHistory.get",
|
||
lambda db, logid: history,
|
||
)
|
||
monkeypatch.setattr(
|
||
"app.api.endpoints.transfer.TransferChain",
|
||
_FakeTransferChain,
|
||
)
|
||
|
||
response = manual_transfer_endpoint(
|
||
transer_item=ManualTransferItem(
|
||
logid=history.id,
|
||
reorganize=True,
|
||
),
|
||
background=False,
|
||
db=object(),
|
||
_="token",
|
||
)
|
||
|
||
assert response.success is True
|
||
assert captured["force"] is True
|
||
assert captured["reorganize"] is True
|
||
assert captured["cleanup_dest_fileitem"] is None
|
||
|
||
|
||
def test_success_history_directory_query_excludes_failed_and_siblings():
|
||
"""目录历史查询应限定路径边界,并且只返回成功记录。"""
|
||
transfer_history_oper = TransferHistoryOper()
|
||
created_histories = [
|
||
transfer_history_oper.add_force(
|
||
src="/issue-6191/show/episode-1.mkv",
|
||
src_storage="local",
|
||
status=True,
|
||
),
|
||
transfer_history_oper.add_force(
|
||
src="/issue-6191/show/episode-2.mkv",
|
||
src_storage="local",
|
||
status=False,
|
||
),
|
||
transfer_history_oper.add_force(
|
||
src="/issue-6191/show-other/episode-3.mkv",
|
||
src_storage="local",
|
||
status=True,
|
||
),
|
||
]
|
||
try:
|
||
histories = transfer_history_oper.list_success_by_src(
|
||
"/issue-6191/show",
|
||
storage="local",
|
||
recursive=True,
|
||
)
|
||
|
||
assert [history.src for history in histories] == [
|
||
"/issue-6191/show/episode-1.mkv"
|
||
]
|
||
finally:
|
||
for history in created_histories:
|
||
transfer_history_oper.delete(history.id)
|
||
|
||
|
||
def test_success_history_directory_query_escapes_sql_wildcards():
|
||
"""目录名中的 SQL 通配符应按普通字符匹配,不能扩大查询范围。"""
|
||
transfer_history_oper = TransferHistoryOper()
|
||
created_histories = [
|
||
transfer_history_oper.add_force(
|
||
src="/issue-6191/show_100%/episode-1.mkv",
|
||
src_storage="local",
|
||
status=True,
|
||
),
|
||
transfer_history_oper.add_force(
|
||
src="/issue-6191/showA100B/episode-2.mkv",
|
||
src_storage="local",
|
||
status=True,
|
||
),
|
||
]
|
||
try:
|
||
histories = transfer_history_oper.list_success_by_src(
|
||
"/issue-6191/show_100%",
|
||
storage="local",
|
||
recursive=True,
|
||
)
|
||
|
||
assert [history.src for history in histories] == [
|
||
"/issue-6191/show_100%/episode-1.mkv"
|
||
]
|
||
finally:
|
||
for history in created_histories:
|
||
transfer_history_oper.delete(history.id)
|
||
|
||
|
||
def test_successful_move_history_is_found_by_current_destination():
|
||
"""成功移动后从媒体库现址打开整理界面时,应识别原整理历史。"""
|
||
transfer_history_oper = TransferHistoryOper()
|
||
destination = make_fileitem("/library/Test Show/Test.Show.S01E01.mkv")
|
||
history = transfer_history_oper.add_force(
|
||
src="/downloads/Test.Show.S01E01.mkv",
|
||
src_storage="local",
|
||
dest=destination.path,
|
||
dest_storage="local",
|
||
dest_fileitem=destination.model_dump(),
|
||
mode="move",
|
||
status=True,
|
||
)
|
||
try:
|
||
histories = TransferChain.get_manual_transfer_histories(
|
||
make_transfer_chain(),
|
||
[destination],
|
||
)
|
||
|
||
assert [current.id for current in histories] == [history.id]
|
||
finally:
|
||
transfer_history_oper.delete(history.id)
|
||
|
||
|
||
def test_manual_transfer_removes_failed_history_before_retry(monkeypatch):
|
||
"""失败历史不应阻塞手动整理,并应在重试前清理旧目标和记录。"""
|
||
chain = make_transfer_chain()
|
||
fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
|
||
old_dest = make_fileitem("/library/Test Show/Test.Show.S01E01.mkv")
|
||
history = SimpleNamespace(
|
||
id=7,
|
||
status=False,
|
||
mode="copy",
|
||
dest_fileitem=old_dest.model_dump(),
|
||
download_hash=None,
|
||
downloader=None,
|
||
src=None,
|
||
src_storage=None,
|
||
)
|
||
planned = []
|
||
deleted = []
|
||
_patch_transfer_planning(
|
||
monkeypatch,
|
||
chain,
|
||
fileitem,
|
||
history,
|
||
planned,
|
||
deleted,
|
||
)
|
||
|
||
state, message = TransferChain.do_transfer(
|
||
chain,
|
||
fileitem=fileitem,
|
||
background=False,
|
||
manual=True,
|
||
)
|
||
|
||
assert state is True
|
||
assert message == ""
|
||
assert deleted == [
|
||
("target", old_dest.path),
|
||
("history", history.id),
|
||
]
|
||
assert planned == [fileitem.path]
|
||
|
||
|
||
def test_automatic_transfer_retries_failed_history_within_retry_budget(monkeypatch):
|
||
"""失败重试次数未达上限(默认计数为 0)时,自动整理遇到失败记录应放行重试,避免一次瞬时故障永久锁死文件。"""
|
||
chain = make_transfer_chain()
|
||
fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
|
||
history = SimpleNamespace(
|
||
id=12,
|
||
status=False,
|
||
mode="copy",
|
||
dest_fileitem=make_fileitem(
|
||
"/library/Test Show/Test.Show.S01E01.mkv"
|
||
).model_dump(),
|
||
download_hash=None,
|
||
downloader=None,
|
||
src=fileitem.path,
|
||
src_storage=fileitem.storage,
|
||
)
|
||
planned = []
|
||
deleted = []
|
||
_patch_transfer_planning(
|
||
monkeypatch,
|
||
chain,
|
||
fileitem,
|
||
history,
|
||
planned,
|
||
deleted,
|
||
)
|
||
|
||
_reset_failed_retries(fileitem.path, fileitem.storage)
|
||
try:
|
||
state, message = TransferChain.do_transfer(
|
||
chain,
|
||
fileitem=fileitem,
|
||
background=False,
|
||
manual=False,
|
||
)
|
||
|
||
assert state is True
|
||
assert message == ""
|
||
assert deleted == []
|
||
assert planned == [fileitem.path]
|
||
finally:
|
||
_reset_failed_retries(fileitem.path, fileitem.storage)
|
||
|
||
|
||
def test_automatic_transfer_skips_failed_history_when_retry_budget_exhausted(monkeypatch):
|
||
"""失败重试次数已达上限时,自动整理遇到失败记录仍应被拦截、不进入整理链,避免失败任务自动循环。"""
|
||
monkeypatch.setattr(settings, "TRANSFER_MAX_FAILED_RETRIES", 1)
|
||
chain = make_transfer_chain()
|
||
fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
|
||
history = SimpleNamespace(
|
||
id=13,
|
||
status=False,
|
||
mode="copy",
|
||
dest_fileitem=make_fileitem(
|
||
"/library/Test Show/Test.Show.S01E01.mkv"
|
||
).model_dump(),
|
||
download_hash=None,
|
||
downloader=None,
|
||
src=fileitem.path,
|
||
src_storage=fileitem.storage,
|
||
)
|
||
planned = []
|
||
deleted = []
|
||
_patch_transfer_planning(
|
||
monkeypatch,
|
||
chain,
|
||
fileitem,
|
||
history,
|
||
planned,
|
||
deleted,
|
||
)
|
||
|
||
_reset_failed_retries(fileitem.path, fileitem.storage)
|
||
try:
|
||
record_transfer_failure(fileitem.path, fileitem.storage)
|
||
|
||
state, message = TransferChain.do_transfer(
|
||
chain,
|
||
fileitem=fileitem,
|
||
background=False,
|
||
manual=False,
|
||
)
|
||
|
||
assert state is False
|
||
assert message == f"{fileitem.name} 已整理过"
|
||
assert deleted == []
|
||
assert planned == []
|
||
finally:
|
||
_reset_failed_retries(fileitem.path, fileitem.storage)
|
||
|
||
|
||
def test_automatic_transfer_new_version_bypasses_exhausted_retry_budget(monkeypatch):
|
||
"""失败预算耗尽后同路径新版本必须进入整理,且不能给下载任务补打已整理标签。"""
|
||
monkeypatch.setattr(settings, "TRANSFER_MAX_FAILED_RETRIES", 1)
|
||
chain = make_transfer_chain()
|
||
fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
|
||
fileitem.size = 200
|
||
history = SimpleNamespace(
|
||
id=15,
|
||
status=False,
|
||
mode="copy",
|
||
dest_fileitem=None,
|
||
download_hash="abc123",
|
||
downloader="qbittorrent",
|
||
src=fileitem.path,
|
||
src_storage=fileitem.storage,
|
||
src_fileitem={"size": 100},
|
||
)
|
||
planned = []
|
||
deleted = []
|
||
_patch_transfer_planning(
|
||
monkeypatch,
|
||
chain,
|
||
fileitem,
|
||
history,
|
||
planned,
|
||
deleted,
|
||
)
|
||
completed = []
|
||
chain.transfer_completed = lambda download_hash, downloader: completed.append(
|
||
(download_hash, downloader)
|
||
)
|
||
|
||
_reset_failed_retries(fileitem.path, fileitem.storage)
|
||
try:
|
||
record_transfer_failure(fileitem.path, fileitem.storage, file_size=100)
|
||
|
||
state, message = TransferChain.do_transfer(
|
||
chain,
|
||
fileitem=fileitem,
|
||
downloader="qbittorrent",
|
||
download_hash="abc123",
|
||
background=False,
|
||
manual=False,
|
||
)
|
||
|
||
assert state is True
|
||
assert message == ""
|
||
assert deleted == []
|
||
assert planned == [fileitem.path]
|
||
assert completed == []
|
||
finally:
|
||
_reset_failed_retries(fileitem.path, fileitem.storage)
|
||
|
||
|
||
def test_manual_transfer_bypasses_retry_budget_when_exhausted(monkeypatch):
|
||
"""
|
||
手动整理不受重试次数上限限制:manual=True 时查重闸根本不参与判定
|
||
(do_transfer 中 `if transferd and not manual` 已把 manual 路径排除在外),
|
||
即使失败计数已达上限(自动路径会因此拦截),仍应清理失败记录、重置计数并放行重试。
|
||
"""
|
||
monkeypatch.setattr(settings, "TRANSFER_MAX_FAILED_RETRIES", 1)
|
||
chain = make_transfer_chain()
|
||
fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
|
||
old_dest = make_fileitem("/library/Test Show/Test.Show.S01E01.mkv")
|
||
history = SimpleNamespace(
|
||
id=14,
|
||
status=False,
|
||
mode="copy",
|
||
dest_fileitem=old_dest.model_dump(),
|
||
download_hash=None,
|
||
downloader=None,
|
||
src=fileitem.path,
|
||
src_storage=fileitem.storage,
|
||
)
|
||
planned = []
|
||
deleted = []
|
||
_patch_transfer_planning(
|
||
monkeypatch,
|
||
chain,
|
||
fileitem,
|
||
history,
|
||
planned,
|
||
deleted,
|
||
)
|
||
|
||
_reset_failed_retries(fileitem.path, fileitem.storage)
|
||
try:
|
||
record_transfer_failure(fileitem.path, fileitem.storage)
|
||
assert failed_retry_count(fileitem.path, fileitem.storage) >= max_failed_retries()
|
||
|
||
state, message = TransferChain.do_transfer(
|
||
chain,
|
||
fileitem=fileitem,
|
||
background=False,
|
||
manual=True,
|
||
)
|
||
|
||
assert state is True
|
||
assert message == ""
|
||
assert deleted == [
|
||
("target", old_dest.path),
|
||
("history", history.id),
|
||
]
|
||
assert planned == [fileitem.path]
|
||
assert failed_retry_count(fileitem.path, fileitem.storage) == 0
|
||
finally:
|
||
_reset_failed_retries(fileitem.path, fileitem.storage)
|
||
|
||
|
||
def test_manual_transfer_keeps_success_history_without_confirmation(monkeypatch):
|
||
"""未携带重整标志时,成功历史仍应阻止普通手动整理。"""
|
||
chain = make_transfer_chain()
|
||
fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
|
||
history = SimpleNamespace(
|
||
id=8,
|
||
status=True,
|
||
mode="copy",
|
||
dest_fileitem=make_fileitem(
|
||
"/library/Test Show/Test.Show.S01E01.mkv"
|
||
).model_dump(),
|
||
download_hash=None,
|
||
downloader=None,
|
||
)
|
||
planned = []
|
||
deleted = []
|
||
_patch_transfer_planning(
|
||
monkeypatch,
|
||
chain,
|
||
fileitem,
|
||
history,
|
||
planned,
|
||
deleted,
|
||
)
|
||
|
||
state, message = TransferChain.do_transfer(
|
||
chain,
|
||
fileitem=fileitem,
|
||
background=False,
|
||
manual=True,
|
||
)
|
||
|
||
assert state is True
|
||
assert message == f"{fileitem.name} 已整理过"
|
||
assert deleted == []
|
||
assert planned == []
|
||
|
||
|
||
def test_manual_reorganize_removes_success_history_and_old_target(monkeypatch):
|
||
"""用户确认重新整理后,应清理成功历史及非移动模式旧目标再执行。"""
|
||
chain = make_transfer_chain()
|
||
fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
|
||
old_dest = make_fileitem("/library/Wrong Show/Wrong.Show.S02E01.mkv")
|
||
history = SimpleNamespace(
|
||
id=9,
|
||
status=True,
|
||
mode="copy",
|
||
dest_fileitem=old_dest.model_dump(),
|
||
download_hash=None,
|
||
downloader=None,
|
||
src=None,
|
||
src_storage=None,
|
||
)
|
||
planned = []
|
||
deleted = []
|
||
_patch_transfer_planning(
|
||
monkeypatch,
|
||
chain,
|
||
fileitem,
|
||
history,
|
||
planned,
|
||
deleted,
|
||
)
|
||
|
||
state, message = TransferChain.do_transfer(
|
||
chain,
|
||
fileitem=fileitem,
|
||
background=False,
|
||
manual=True,
|
||
reorganize=True,
|
||
)
|
||
|
||
assert state is True
|
||
assert message == ""
|
||
assert deleted == [
|
||
("target", old_dest.path),
|
||
("history", history.id),
|
||
]
|
||
assert planned == [fileitem.path]
|
||
|
||
|
||
def test_manual_reorganize_keeps_successful_move_target_as_source(monkeypatch):
|
||
"""成功移动后的目标是当前重整源,只能删历史记录,不能先删除文件。"""
|
||
chain = make_transfer_chain()
|
||
fileitem = make_fileitem("/library/Test Show/Test.Show.S01E01.mkv")
|
||
history = SimpleNamespace(
|
||
id=10,
|
||
status=True,
|
||
mode="move",
|
||
dest_fileitem=fileitem.model_dump(),
|
||
download_hash=None,
|
||
downloader=None,
|
||
src=None,
|
||
src_storage=None,
|
||
)
|
||
planned = []
|
||
deleted = []
|
||
_patch_transfer_planning(
|
||
monkeypatch,
|
||
chain,
|
||
fileitem,
|
||
history,
|
||
planned,
|
||
deleted,
|
||
)
|
||
|
||
state, message = TransferChain.do_transfer(
|
||
chain,
|
||
fileitem=fileitem,
|
||
background=False,
|
||
manual=True,
|
||
reorganize=True,
|
||
)
|
||
|
||
assert state is True
|
||
assert message == ""
|
||
assert deleted == [("history", history.id)]
|
||
assert planned == [fileitem.path]
|
||
|
||
|
||
def test_forced_manual_reorganize_still_removes_history(monkeypatch):
|
||
"""从历史入口强制重新整理时,仍应按重整确认清理旧目标和记录。"""
|
||
chain = make_transfer_chain()
|
||
fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
|
||
old_dest = make_fileitem("/library/Wrong Show/Wrong.Show.S02E01.mkv")
|
||
history = SimpleNamespace(
|
||
id=11,
|
||
status=True,
|
||
mode="copy",
|
||
dest_fileitem=old_dest.model_dump(),
|
||
download_hash=None,
|
||
downloader=None,
|
||
src=None,
|
||
src_storage=None,
|
||
)
|
||
planned = []
|
||
deleted = []
|
||
_patch_transfer_planning(
|
||
monkeypatch,
|
||
chain,
|
||
fileitem,
|
||
history,
|
||
planned,
|
||
deleted,
|
||
)
|
||
|
||
state, message = TransferChain.do_transfer(
|
||
chain,
|
||
fileitem=fileitem,
|
||
background=False,
|
||
manual=True,
|
||
force=True,
|
||
reorganize=True,
|
||
)
|
||
|
||
assert state is True
|
||
assert message == ""
|
||
assert deleted == [
|
||
("target", old_dest.path),
|
||
("history", history.id),
|
||
]
|
||
assert planned == [fileitem.path]
|