Files
2026-07-13 11:59:58 +08:00

136 lines
5.0 KiB
Python

"""Regression anchor: uploads router must not block the event loop."""
from __future__ import annotations
import asyncio
from io import BytesIO
from pathlib import Path
from types import SimpleNamespace
import pytest
from _router_auth_helpers import call_unwrapped
from fastapi import UploadFile
from app.gateway.routers import uploads
from deerflow.runtime.user_context import get_effective_user_id
from deerflow.uploads.manager import ensure_uploads_dir, get_uploads_dir
pytestmark = pytest.mark.asyncio
class _SandboxRecorder:
def __init__(self) -> None:
self.updates: list[tuple[str, bytes]] = []
def update_file(self, path: str, content: bytes) -> None:
self.updates.append((path, content))
class _MountedProvider:
uses_thread_data_mounts = True
def acquire(self, thread_id: str | None = None, *, user_id: str | None = None) -> str:
raise AssertionError("mounted upload path must not acquire a sandbox")
async def acquire_async(self, thread_id: str | None = None, *, user_id: str | None = None) -> str:
raise AssertionError("mounted upload path must not acquire a sandbox")
def get(self, sandbox_id: str):
raise AssertionError("mounted upload path must not read a sandbox")
class _RemoteProvider:
uses_thread_data_mounts = False
def __init__(self) -> None:
self.sandbox = _SandboxRecorder()
self.acquire_async_calls: list[tuple[str | None, str | None]] = []
def acquire(self, thread_id: str | None = None, *, user_id: str | None = None) -> str:
raise AssertionError("upload route should use acquire_async")
async def acquire_async(self, thread_id: str | None = None, *, user_id: str | None = None) -> str:
self.acquire_async_calls.append((thread_id, user_id))
return "remote-sandbox"
def get(self, sandbox_id: str):
if sandbox_id == "remote-sandbox":
return self.sandbox
return None
def _reset_paths(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path))
import deerflow.config.paths as paths_mod
monkeypatch.setattr(paths_mod, "_paths", None)
async def _thread_uploads_dir(thread_id: str, *, user_id: str | None = None) -> Path:
user_id = user_id or get_effective_user_id()
return await asyncio.to_thread(ensure_uploads_dir, thread_id, user_id=user_id)
async def test_upload_endpoint_mounted_provider_does_not_block_event_loop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
_reset_paths(tmp_path, monkeypatch)
provider = _MountedProvider()
monkeypatch.setattr(uploads, "get_sandbox_provider", lambda: provider)
result = await call_unwrapped(
uploads.upload_files,
"t-mounted",
request=None,
files=[UploadFile(filename="notes.txt", file=BytesIO(b"hello uploads"))],
config=SimpleNamespace(),
)
user_id = get_effective_user_id()
target = await asyncio.to_thread(lambda: get_uploads_dir("t-mounted", user_id=user_id) / "notes.txt")
assert result.success is True
assert result.files[0].filename == "notes.txt"
assert await asyncio.to_thread(target.read_bytes) == b"hello uploads"
async def test_upload_endpoint_remote_provider_syncs_without_blocking_event_loop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
_reset_paths(tmp_path, monkeypatch)
provider = _RemoteProvider()
monkeypatch.setattr(uploads, "get_sandbox_provider", lambda: provider)
monkeypatch.setattr(uploads, "get_effective_user_id", lambda: "owner-upload")
result = await call_unwrapped(
uploads.upload_files,
"t-remote",
request=None,
files=[UploadFile(filename="report.txt", file=BytesIO(b"remote bytes"))],
config=SimpleNamespace(),
)
assert result.success is True
assert provider.acquire_async_calls == [("t-remote", "owner-upload")]
assert provider.sandbox.updates == [("/mnt/user-data/uploads/report.txt", b"remote bytes")]
async def test_list_uploaded_files_does_not_block_event_loop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
_reset_paths(tmp_path, monkeypatch)
uploads_dir = await _thread_uploads_dir("t-list")
await asyncio.to_thread((uploads_dir / "notes.txt").write_bytes, b"hello")
result = await call_unwrapped(uploads.list_uploaded_files, "t-list", request=None)
assert result.count == 1
assert result.files[0].filename == "notes.txt"
assert result.files[0].size == len(b"hello")
async def test_delete_uploaded_file_does_not_block_event_loop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
_reset_paths(tmp_path, monkeypatch)
uploads_dir = await _thread_uploads_dir("t-delete")
target = uploads_dir / "notes.txt"
await asyncio.to_thread(target.write_bytes, b"delete me")
result = await call_unwrapped(uploads.delete_uploaded_file, "t-delete", "notes.txt", request=None)
assert result == {"success": True, "message": "Deleted notes.txt"}
assert not await asyncio.to_thread(target.exists)