"""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 == []