fix(agent): isolate web and storage blocking I/O

This commit is contained in:
InfinityPacer
2026-08-23 13:06:30 +08:00
parent 43c173a0e7
commit d11c2fb301
7 changed files with 558 additions and 56 deletions
+125
View File
@@ -1,4 +1,5 @@
import asyncio
import threading
from types import SimpleNamespace
from app.agent.tools.impl.delete_transfer_history import DeleteTransferHistoryTool
@@ -277,6 +278,130 @@ def test_delete_transfer_history_tool_only_treats_exact_move_as_reorganize_sourc
]
def test_delete_transfer_history_storage_work_runs_outside_event_loop(monkeypatch):
"""整理历史的本地存储操作应在 storage worker 中执行。"""
caller_thread = threading.get_ident()
storage_threads = []
history = SimpleNamespace(
id=15,
title="奔跑吧",
src="/downloads/Keep.Running.mkv",
status=True,
mode="copy",
dest_fileitem={
"storage": "local",
"path": "/library/奔跑吧 (2014)/Keep.Running.mkv",
"name": "Keep.Running.mkv",
"type": "file",
},
)
class FakeTransferHistoryOper:
async def async_get(self, history_id):
return history
async def async_delete(self, history_id):
return None
class FakeStorageChain:
def exists(self, fileitem):
storage_threads.append(threading.get_ident())
return True
def delete_media_file(self, fileitem):
storage_threads.append(threading.get_ident())
return True
monkeypatch.setattr(
"app.agent.tools.impl.delete_transfer_history.TransferHistoryOper",
FakeTransferHistoryOper,
)
monkeypatch.setattr(
"app.agent.tools.impl.delete_transfer_history.StorageChain",
FakeStorageChain,
)
result = asyncio.run(
DeleteTransferHistoryTool(
session_id="redo-session",
user_id="10001",
).run(history_id=15)
)
assert "已删除整理历史记录" in result
assert storage_threads
assert all(thread_id != caller_thread for thread_id in storage_threads)
def test_delete_transfer_history_cancellation_keeps_history_record(monkeypatch):
"""取消等待存储清理时不得继续提交整理历史删除。"""
started = threading.Event()
release = threading.Event()
finished = threading.Event()
history = SimpleNamespace(
id=16,
title="奔跑吧",
src="/downloads/Keep.Running.mkv",
status=True,
mode="copy",
dest_fileitem={
"storage": "local",
"path": "/library/奔跑吧 (2014)/Keep.Running.mkv",
"name": "Keep.Running.mkv",
"type": "file",
},
)
delete_history_calls = []
class FakeTransferHistoryOper:
async def async_get(self, history_id):
return history
async def async_delete(self, history_id):
delete_history_calls.append(history_id)
class FakeStorageChain:
def exists(self, fileitem):
started.set()
release.wait(timeout=1)
return True
def delete_media_file(self, fileitem):
finished.set()
return True
monkeypatch.setattr(
"app.agent.tools.impl.delete_transfer_history.TransferHistoryOper",
FakeTransferHistoryOper,
)
monkeypatch.setattr(
"app.agent.tools.impl.delete_transfer_history.StorageChain",
FakeStorageChain,
)
async def scenario():
task = asyncio.create_task(
DeleteTransferHistoryTool(
session_id="redo-session",
user_id="10001",
).run(history_id=16)
)
assert await asyncio.to_thread(started.wait, 1)
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
try:
asyncio.run(scenario())
finally:
release.set()
assert delete_history_calls == []
assert finished.wait(timeout=1)
def test_manual_redo_context_uses_dest_path_for_successful_move_record():
"""成功 move 记录重新整理时,旧目标文件才是可继续整理的输入路径。"""
history = SimpleNamespace(