mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-03 06:12:14 +08:00
* Fix Windows backend test compatibility * Preserve ACP path style on Windows * Fix installer import ordering * Address review comments for Windows fixes --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
106 lines
4.5 KiB
Python
106 lines
4.5 KiB
Python
"""Tests for AioSandboxProvider mount helpers."""
|
|
|
|
import importlib
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from deerflow.config.paths import Paths
|
|
|
|
# ── ensure_thread_dirs ───────────────────────────────────────────────────────
|
|
|
|
|
|
def test_ensure_thread_dirs_creates_acp_workspace(tmp_path):
|
|
"""ACP workspace directory must be created alongside user-data dirs."""
|
|
paths = Paths(base_dir=tmp_path)
|
|
paths.ensure_thread_dirs("thread-1")
|
|
|
|
assert (tmp_path / "threads" / "thread-1" / "user-data" / "workspace").exists()
|
|
assert (tmp_path / "threads" / "thread-1" / "user-data" / "uploads").exists()
|
|
assert (tmp_path / "threads" / "thread-1" / "user-data" / "outputs").exists()
|
|
assert (tmp_path / "threads" / "thread-1" / "acp-workspace").exists()
|
|
|
|
|
|
def test_ensure_thread_dirs_acp_workspace_is_world_writable(tmp_path):
|
|
"""ACP workspace must be chmod 0o777 so the ACP subprocess can write into it."""
|
|
paths = Paths(base_dir=tmp_path)
|
|
paths.ensure_thread_dirs("thread-2")
|
|
|
|
acp_dir = tmp_path / "threads" / "thread-2" / "acp-workspace"
|
|
mode = oct(acp_dir.stat().st_mode & 0o777)
|
|
assert mode == oct(0o777)
|
|
|
|
|
|
# ── _get_thread_mounts ───────────────────────────────────────────────────────
|
|
|
|
|
|
def _make_provider(tmp_path):
|
|
"""Build a minimal AioSandboxProvider instance without starting the idle checker."""
|
|
aio_mod = importlib.import_module("deerflow.community.aio_sandbox.aio_sandbox_provider")
|
|
with patch.object(aio_mod.AioSandboxProvider, "_start_idle_checker"):
|
|
provider = aio_mod.AioSandboxProvider.__new__(aio_mod.AioSandboxProvider)
|
|
provider._config = {}
|
|
provider._sandboxes = {}
|
|
provider._lock = MagicMock()
|
|
provider._idle_checker_stop = MagicMock()
|
|
return provider
|
|
|
|
|
|
def test_get_thread_mounts_includes_acp_workspace(tmp_path, monkeypatch):
|
|
"""_get_thread_mounts must include /mnt/acp-workspace (read-only) for docker sandbox."""
|
|
aio_mod = importlib.import_module("deerflow.community.aio_sandbox.aio_sandbox_provider")
|
|
monkeypatch.setattr(aio_mod, "get_paths", lambda: Paths(base_dir=tmp_path))
|
|
|
|
mounts = aio_mod.AioSandboxProvider._get_thread_mounts("thread-3")
|
|
|
|
container_paths = {m[1]: (m[0], m[2]) for m in mounts}
|
|
|
|
assert "/mnt/acp-workspace" in container_paths, "ACP workspace mount is missing"
|
|
expected_host = str(tmp_path / "threads" / "thread-3" / "acp-workspace")
|
|
actual_host, read_only = container_paths["/mnt/acp-workspace"]
|
|
assert actual_host == expected_host
|
|
assert read_only is True, "ACP workspace should be read-only inside the sandbox"
|
|
|
|
|
|
def test_get_thread_mounts_includes_user_data_dirs(tmp_path, monkeypatch):
|
|
"""Baseline: user-data mounts must still be present after the ACP workspace change."""
|
|
aio_mod = importlib.import_module("deerflow.community.aio_sandbox.aio_sandbox_provider")
|
|
monkeypatch.setattr(aio_mod, "get_paths", lambda: Paths(base_dir=tmp_path))
|
|
|
|
mounts = aio_mod.AioSandboxProvider._get_thread_mounts("thread-4")
|
|
container_paths = {m[1] for m in mounts}
|
|
|
|
assert "/mnt/user-data/workspace" in container_paths
|
|
assert "/mnt/user-data/uploads" in container_paths
|
|
assert "/mnt/user-data/outputs" in container_paths
|
|
|
|
|
|
def test_discover_or_create_only_unlocks_when_lock_succeeds(tmp_path, monkeypatch):
|
|
"""Unlock should not run if exclusive locking itself fails."""
|
|
aio_mod = importlib.import_module("deerflow.community.aio_sandbox.aio_sandbox_provider")
|
|
provider = _make_provider(tmp_path)
|
|
provider._discover_or_create_with_lock = aio_mod.AioSandboxProvider._discover_or_create_with_lock.__get__(
|
|
provider,
|
|
aio_mod.AioSandboxProvider,
|
|
)
|
|
|
|
monkeypatch.setattr(aio_mod, "get_paths", lambda: Paths(base_dir=tmp_path))
|
|
monkeypatch.setattr(
|
|
aio_mod,
|
|
"_lock_file_exclusive",
|
|
lambda _lock_file: (_ for _ in ()).throw(RuntimeError("lock failed")),
|
|
)
|
|
|
|
unlock_calls: list[object] = []
|
|
monkeypatch.setattr(
|
|
aio_mod,
|
|
"_unlock_file",
|
|
lambda lock_file: unlock_calls.append(lock_file),
|
|
)
|
|
|
|
with patch.object(provider, "_create_sandbox", return_value="sandbox-id"):
|
|
with pytest.raises(RuntimeError, match="lock failed"):
|
|
provider._discover_or_create_with_lock("thread-5", "sandbox-5")
|
|
|
|
assert unlock_calls == []
|