deer-flow/backend/tests/blocking_io/test_dingtalk_receive_file.py
2026-08-06 10:42:34 +08:00

81 lines
3.1 KiB
Python

"""Regression anchor: DingTalk ``receive_file`` must not block the event loop.
``_receive_single_file`` prepares the thread directories, resolves the uploads
dir, stages the attachment, and publishes it without replacing an existing name
— all blocking filesystem IO that must run inside ``asyncio.to_thread`` (and
sandbox sync must go through ``acquire_async`` + an offloaded ``update_file``).
This anchor drives the real ``receive_file`` under the strict Blockbuster gate;
if any of that regresses back onto the event loop, Blockbuster raises
``BlockingError``.
The ``Paths`` construction is offloaded only because ``Paths.__init__`` resolves
paths synchronously; the surface under test (``receive_file``'s persist path) is
exercised on the event loop, not bypassed. The download itself is mocked — the
network leg is httpx-async and not the subject here.
"""
from __future__ import annotations
import asyncio
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
pytestmark = pytest.mark.asyncio
async def test_receive_file_persist_does_not_block_event_loop(tmp_path, monkeypatch) -> None:
from app.channels.dingtalk import DingTalkChannel
from app.channels.message_bus import MessageBus
from deerflow.config.paths import Paths
paths = await asyncio.to_thread(Paths, str(tmp_path))
monkeypatch.setattr("app.channels.dingtalk.get_paths", lambda: paths)
async def _acquire_async(thread_id, user_id=None):
return "local"
monkeypatch.setattr(
"app.channels.dingtalk.get_sandbox_provider",
lambda: SimpleNamespace(acquire_async=_acquire_async, get=lambda sid: None),
)
channel = DingTalkChannel(MessageBus(), config={})
channel._download_by_code = AsyncMock(return_value=b"DATA")
msg = channel._make_inbound(
chat_id="c",
user_id="u",
text="hi",
thread_ts="m",
files=[{"type": "file", "download_code": "dc", "filename": "a.pdf"}],
)
out = await channel.receive_file(msg, "t1", user_id="default")
assert "/uploads/a.pdf" in out.text
assert out.files == []
async def test_receive_file_rejects_symlinked_upload_directory(tmp_path, monkeypatch) -> None:
from app.channels.dingtalk import DingTalkChannel
from app.channels.message_bus import MessageBus
from deerflow.config.paths import Paths
paths = await asyncio.to_thread(Paths, str(tmp_path))
await asyncio.to_thread(paths.ensure_thread_dirs, "t1", user_id="default")
uploads = paths.sandbox_uploads_dir("t1", user_id="default")
outside = tmp_path / "outside"
await asyncio.to_thread(outside.mkdir)
await asyncio.to_thread(uploads.rmdir)
await asyncio.to_thread(uploads.symlink_to, outside, target_is_directory=True)
monkeypatch.setattr("app.channels.dingtalk.get_paths", lambda: paths)
channel = DingTalkChannel(MessageBus(), config={})
channel._download_by_code = AsyncMock(return_value=b"DATA")
result = await channel._receive_single_file("dc", "file", "a.pdf", "t1", user_id="default")
assert result == ""
assert not await asyncio.to_thread((outside / "a.pdf").exists)