"""Unit tests for the BoxLite community provider. These run in CI without BoxLite installed: they cover the lazy-import error path, provider lifecycle, the path-safety guards, and the warm pool lifecycle — none of which need a live box. """ from __future__ import annotations import asyncio import hashlib import logging import os import shutil import subprocess import sys import threading import time import types import pytest from deerflow.community.boxlite.box import BoxliteBox from deerflow.community.boxlite.provider import BoxliteProvider, _import_simplebox from deerflow.trace_context import get_current_trace_id, request_trace_context _LEGACY_COLLIDING_IDENTITIES = ( ("user-9721", "thread-9721"), ("user-94361", "thread-94361"), ) # ── Fake BoxLite SDK ────────────────────────────────────────────────── class _FakeBox: """A fake SimpleBox that records lifecycle calls without starting real VMs.""" def __init__(self, *, image=None, name=None, memory_mib=None, cpus=None, **kwargs): self.id = name or "auto-gen-id" self.name = name self._image = image self._started = False self._stopped = False self._exec_history: list[tuple] = [] async def start(self): self._started = True async def exec(self, *argv, env=None, timeout=None): self._exec_history.append((argv, env, timeout)) _FakeResult = type("_FakeResult", (), {"stdout": "", "stderr": "", "exit_code": 0}) # Health check: box.execute_command("echo ok") → exec("sh", "-lc", "echo ok") if len(argv) >= 3 and argv[0] == "sh" and argv[1] == "-lc" and argv[2] == "echo ok": return type("_FakeResult", (), {"stdout": "ok\n", "stderr": "", "exit_code": 0})() if len(argv) >= 3 and argv[0] == "sh" and argv[1] == "-lc" and "__DF_SEARCH_STATUS__:" in argv[2]: # A search that ran and found nothing (see sandbox/remote_search.py). return type("_FakeResult", (), {"stdout": "\n__DF_SEARCH_STATUS__:1\n", "stderr": "", "exit_code": 0})() return _FakeResult() async def stop(self): self._stopped = True def _fake_run(coro, *, timeout=None): """Sync runner that executes coroutines on a temporary event loop (no daemon thread).""" return asyncio.run(coro) # ── Config stub ─────────────────────────────────────────────────────── def _stub_config(sandbox_attrs=None): """Stub get_app_config to return a config with given sandbox attrs.""" attrs = sandbox_attrs or {} stub = types.SimpleNamespace(sandbox=types.SimpleNamespace(**attrs)) return stub def _no_boxlite(monkeypatch: pytest.MonkeyPatch) -> None: """Make ``import boxlite`` raise, regardless of whether it is installed.""" monkeypatch.setitem(sys.modules, "boxlite", None) @pytest.fixture(autouse=True) def _no_existing_boxlite_boxes(monkeypatch: pytest.MonkeyPatch) -> None: """Keep provider startup reconciliation isolated from the real SDK state.""" class _EmptyRuntime: def start(self): return self def stop(self): pass def list_info(self): return [] class _EmptyBoxlite: @staticmethod def default(): return _EmptyRuntime() monkeypatch.setattr("deerflow.community.boxlite.provider._import_sync_boxlite_runtime", lambda: _EmptyBoxlite) def test_import_simplebox_missing_raises_actionable(monkeypatch: pytest.MonkeyPatch) -> None: _no_boxlite(monkeypatch) with pytest.raises(ImportError, match=r"deerflow-harness\[boxlite\]"): _import_simplebox() def test_acquire_without_boxlite_raises_and_shuts_down_cleanly(monkeypatch: pytest.MonkeyPatch) -> None: # Stub config so the provider constructs without a config.yaml on disk. stub = types.SimpleNamespace(sandbox=types.SimpleNamespace()) monkeypatch.setattr("deerflow.community.boxlite.provider.get_app_config", lambda: stub) _no_boxlite(monkeypatch) provider = BoxliteProvider() try: with pytest.raises(ImportError, match=r"deerflow-harness\[boxlite\]"): provider.acquire("thread-1", user_id="u") finally: provider.shutdown() # must not raise even though no box was ever created # Idempotent shutdown. provider.shutdown() def test_guard_traversal() -> None: assert BoxliteBox._guard_traversal("/mnt/user-data/workspace/a.txt") == "/mnt/user-data/workspace/a.txt" assert BoxliteBox._guard_traversal("relative/ok.txt") == "relative/ok.txt" with pytest.raises(PermissionError): BoxliteBox._guard_traversal("/mnt/user-data/../etc/passwd") with pytest.raises(ValueError): BoxliteBox._guard_traversal("") def test_download_file_guards_reject_before_touching_box() -> None: # ``run`` must never be called: both guards raise before any exec. def _fail_run(_coro: object) -> None: raise AssertionError("download_file must reject the path before running a command") box = BoxliteBox("box-id", box=object(), run=_fail_run) with pytest.raises(PermissionError): box.download_file("/etc/passwd") # outside the /mnt/user-data prefix with pytest.raises(PermissionError): box.download_file("/mnt/user-data/../etc/passwd") # traversal def test_execute_command_rejects_invalid_env_key() -> None: def _fail_run(_coro: object) -> None: raise AssertionError("execute_command must reject a bad env key before running") box = BoxliteBox("box-id", box=object(), run=_fail_run) with pytest.raises(ValueError, match=r"POSIX"): box.execute_command("echo hi", env={"BAD KEY": "x"}) def test_execute_command_forwards_timeout_to_sdk_and_loop_runner() -> None: """Command timeout must bound both BoxLite exec and the loop bridge future.""" run_timeouts: list[float | None] = [] def _recording_run(coro, *, timeout=None): run_timeouts.append(timeout) return asyncio.run(coro) fake = _FakeBox(name="box-id") box = BoxliteBox("box-id", box=fake, run=_recording_run) output = box.execute_command("echo ok", timeout=5) assert "ok" in output assert fake._exec_history[-1] == (("sh", "-lc", "echo ok"), None, 5) assert run_timeouts == [5] def test_execute_command_appends_exit_marker_when_failure_has_output(monkeypatch: pytest.MonkeyPatch) -> None: """LocalSandbox parity: a nonzero exit survives in the output text even when the command produced output (acceptance-checklist evidence).""" box = BoxliteBox("box-id", box=object(), run=_fake_run) monkeypatch.setattr( box, "_exec", lambda *argv, **kwargs: types.SimpleNamespace(exit_code=1, stdout="5 passed, 1 error\n", stderr=""), ) assert box.execute_command("make test") == "5 passed, 1 error\n\nExit Code: 1" def test_read_file_supports_optional_line_ranges(monkeypatch: pytest.MonkeyPatch) -> None: calls: list[tuple[str, ...]] = [] box = BoxliteBox("box-id", box=object(), run=_fake_run) def _fake_exec(*argv: str): calls.append(argv) return types.SimpleNamespace( exit_code=0, stdout="line 1\nline 2\nline 3\nline 4\nline 5", stderr="", ) monkeypatch.setattr(box, "_exec", _fake_exec) assert box.read_file("/mnt/user-data/workspace/range.txt") == "line 1\nline 2\nline 3\nline 4\nline 5" assert box.read_file("/mnt/user-data/workspace/range.txt", start_line=2, end_line=4) == "line 2\nline 3\nline 4" assert box.read_file("/mnt/user-data/workspace/range.txt", start_line=4) == "line 4\nline 5" assert box.read_file("/mnt/user-data/workspace/range.txt", end_line=2) == "line 1\nline 2" assert all(call == ("cat", "--", "/mnt/user-data/workspace/range.txt") for call in calls) def test_grep_always_prints_filename_for_single_file_paths() -> None: fake = _FakeBox(name="box-id") box = BoxliteBox("box-id", box=fake, run=_fake_run) box.grep("/mnt/user-data/uploads/report.md", "needle") grep_commands = [argv[0][2] for argv in fake._exec_history if argv[0][:2] == ("sh", "-lc") and "grep -r " in argv[0][2]] assert grep_commands assert "-H" in grep_commands[-1].split() def test_execute_command_invalidates_box_on_terminal_transport_error() -> None: invalidated: list[tuple[str, str]] = [] def _failing_run(coro, *, timeout=None): coro.close() raise RuntimeError("vsock disconnected") box = BoxliteBox( "box-id", box=_FakeBox(name="box-id"), run=_failing_run, on_terminal_failure=lambda sandbox_id, reason: invalidated.append((sandbox_id, reason)), ) output = box.execute_command("echo hi") assert output == "Error: vsock disconnected" assert invalidated == [("box-id", "vsock disconnected")] def test_execute_command_does_not_invalidate_on_regular_command_error() -> None: invalidated: list[tuple[str, str]] = [] def _failing_run(coro, *, timeout=None): coro.close() raise RuntimeError("user command failed") box = BoxliteBox( "box-id", box=_FakeBox(name="box-id"), run=_failing_run, on_terminal_failure=lambda sandbox_id, reason: invalidated.append((sandbox_id, reason)), ) output = box.execute_command("echo hi") assert output == "Error: user command failed" assert invalidated == [] def test_execute_command_does_not_invalidate_on_retryable_transport_message() -> None: invalidated: list[tuple[str, str]] = [] def _failing_run(coro, *, timeout=None): coro.close() raise RuntimeError("transport not ready, retry later") box = BoxliteBox( "box-id", box=_FakeBox(name="box-id"), run=_failing_run, on_terminal_failure=lambda sandbox_id, reason: invalidated.append((sandbox_id, reason)), ) output = box.execute_command("echo hi") assert output == "Error: transport not ready, retry later" assert invalidated == [] def test_execute_command_uses_overridable_terminal_markers(monkeypatch: pytest.MonkeyPatch) -> None: invalidated: list[tuple[str, str]] = [] monkeypatch.setattr(BoxliteBox, "TERMINAL_ERROR_MARKERS", ("custom terminal marker",)) monkeypatch.setattr(BoxliteBox, "RETRYABLE_ERROR_MARKERS", ()) def _failing_run(coro, *, timeout=None): coro.close() raise RuntimeError("custom terminal marker") box = BoxliteBox( "box-id", box=_FakeBox(name="box-id"), run=_failing_run, on_terminal_failure=lambda sandbox_id, reason: invalidated.append((sandbox_id, reason)), ) output = box.execute_command("echo hi") assert output == "Error: custom terminal marker" assert invalidated == [("box-id", "custom terminal marker")] def test_execute_command_closed_box_returns_without_error_log(caplog) -> None: box = BoxliteBox("box-id", box=_FakeBox(name="box-id"), run=_fake_run) box.close() with caplog.at_level(logging.ERROR, logger="deerflow.community.boxlite.box"): output = box.execute_command("echo hi") assert output == "Error: sandbox has been closed" assert "Failed to execute command in BoxLite box" not in caplog.text def test_sandbox_id_deterministic(monkeypatch): """_sandbox_id produces the same id for the same inputs.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) provider = BoxliteProvider() id1 = provider._sandbox_id("thread-1", "user-a") id2 = provider._sandbox_id("thread-1", "user-a") assert id1 == id2 assert len(id1) == 16 def test_sandbox_id_different_users(monkeypatch): """Different users produce different ids for the same thread.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) provider = BoxliteProvider() id_a = provider._sandbox_id("thread-1", "user-a") id_b = provider._sandbox_id("thread-1", "user-b") assert id_a != id_b def test_sandbox_id_different_threads(monkeypatch): """Different threads produce different ids for the same user.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) provider = BoxliteProvider() id_a = provider._sandbox_id("thread-1", "user-a") id_b = provider._sandbox_id("thread-2", "user-a") assert id_a != id_b def test_idle_timeout_zero_is_preserved_and_disables_reaper(monkeypatch): """idle_timeout=0 is a valid config value and disables the reaper thread.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config({"idle_timeout": 0}), ) provider = BoxliteProvider() assert provider._config["idle_timeout"] == 0 assert provider._idle_checker_thread is None provider.shutdown() def test_create_box_passes_prefixed_sandbox_id_as_name(monkeypatch): """_create_box gives BoxLite a DeerFlow-owned name prefix.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) # Inject fake SimpleBox and fake loop runner created_boxes = [] class _RecordingBox(_FakeBox): def __init__(self, **kwargs): super().__init__(**kwargs) created_boxes.append(kwargs) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _RecordingBox, ) provider = BoxliteProvider() # Replace _loop.run with our sync runner provider._loop.run = _fake_run box = provider._create_box("test-sandbox-id") assert len(created_boxes) == 1 assert created_boxes[0]["name"] == "deer-flow-boxlite-test-sandbox-id" assert box.id == "test-sandbox-id" def test_startup_reconciliation_adopts_prefixed_existing_boxes(monkeypatch): """Existing DeerFlow-named BoxLite boxes are adopted into the warm pool.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) stopped: list[str] = [] class _NativeBox: def stop(self): stopped.append("adopted") class _Runtime: def start(self): return self def stop(self): pass def list_info(self): return [ types.SimpleNamespace(name="deer-flow-boxlite-adopted"), types.SimpleNamespace(name="unrelated-box"), types.SimpleNamespace(name=None), ] def get(self, name): if name == "deer-flow-boxlite-adopted": return _NativeBox() raise AssertionError(f"unexpected box lookup: {name}") class _Boxlite: @staticmethod def default(): return _Runtime() monkeypatch.setattr("deerflow.community.boxlite.provider._import_sync_boxlite_runtime", lambda: _Boxlite) provider = BoxliteProvider() assert list(provider._warm_pool) == ["adopted"] adopted_box = provider._warm_pool["adopted"][0] assert adopted_box.id == "adopted" provider.shutdown() assert stopped == ["adopted"] def test_release_parks_in_warm_pool(monkeypatch): """After release, box is in warm pool, not destroyed.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run # Acquire a box sid = provider.acquire("thread-1", user_id="u1") # Verify box is active assert sid in provider._boxes assert sid not in provider._warm_pool # Release provider.release(sid) # Verify box is in warm pool, not active assert sid not in provider._boxes assert sid in provider._warm_pool box, ts = provider._warm_pool[sid] assert isinstance(box, BoxliteBox) assert not box._box._stopped # VM not destroyed def test_acquire_reclaims_from_warm_pool(monkeypatch): """acquire reclaims a warm pool box for the same thread.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run # First acquire → create sid1 = provider.acquire("thread-1", user_id="u1") provider.release(sid1) # Second acquire → should reclaim from warm pool sid2 = provider.acquire("thread-1", user_id="u1") assert sid1 == sid2 # Same deterministic ID assert sid2 in provider._boxes assert sid2 not in provider._warm_pool @pytest.mark.asyncio async def test_acquire_async_propagates_request_trace_context(monkeypatch): """acquire_async must carry request ContextVars into the worker thread. Regression test (#5089): the dedicated-executor bridge replaced the inherited ``to_thread`` acquire (which copies contextvars). Without an explicit ``copy_context`` the worker thread reads the trace id bound by ``request_trace_context()`` as unset and logs ``trace_id=-``. """ monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run seen: list[str | None] = [] original = BoxliteProvider._acquire_scope_locked def spy(self, key, sandbox_id): # Runs inside the executor worker thread, under the copied context. seen.append(get_current_trace_id()) return original(self, key, sandbox_id) monkeypatch.setattr(BoxliteProvider, "_acquire_scope_locked", spy) try: with request_trace_context("trace-boxlite-5089"): sid = await provider.acquire_async("thread-1", user_id="u1") assert sid assert seen == ["trace-boxlite-5089"] finally: # _fake_run uses asyncio.run, which cannot run inside this test's event # loop thread; shut down on a worker thread so box close() completes. await asyncio.to_thread(provider.shutdown) def test_explicit_recent_reclaim_skip_avoids_health_check(monkeypatch): """A configured skip window can reclaim recently released boxes without a ping.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config({"health_check_skip_seconds": 5}), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run sid = provider.acquire("thread-1", user_id="u1") provider.release(sid) box, _ = provider._warm_pool[sid] def _fail_if_called(*args, **kwargs): raise AssertionError("health check should be skipped for recently released boxes") monkeypatch.setattr(box, "execute_command", _fail_if_called) reclaimed = provider._reclaim_warm_pool(sid, ("u1", "thread-1")) assert reclaimed == sid assert sid in provider._boxes assert sid not in provider._warm_pool provider.shutdown() def test_recent_reclaim_validates_by_default(monkeypatch): monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run sid = provider.acquire("thread-1", user_id="u1") provider.release(sid) box, _ = provider._warm_pool[sid] calls = 0 original_execute = box.execute_command def _record_health_check(command: str, *args, **kwargs): nonlocal calls calls += 1 return original_execute(command, *args, **kwargs) monkeypatch.setattr(box, "execute_command", _record_health_check) reclaimed = provider._reclaim_warm_pool(sid, ("u1", "thread-1")) assert reclaimed == sid assert calls == 1 assert sid in provider._boxes assert sid not in provider._warm_pool provider.shutdown() def test_default_recent_reclaim_drops_dead_warm_box(monkeypatch): monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run sid = provider.acquire("thread-1", user_id="u1") provider.release(sid) box, _ = provider._warm_pool[sid] def _dead_health_check(command: str, *args, **kwargs): assert command == "echo ok" return "Error: vsock disconnected" monkeypatch.setattr(box, "execute_command", _dead_health_check) reclaimed = provider._reclaim_warm_pool(sid, ("u1", "thread-1")) assert reclaimed is None assert sid not in provider._boxes assert sid not in provider._warm_pool assert sid not in provider._skip_health_check_warm_ids assert box.is_closed is True provider.shutdown() def test_dead_active_box_invalidation_closes_adapter(monkeypatch): monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config({"health_check_skip_seconds": 5}), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run sid = provider.acquire("thread-1", user_id="u1") box = provider.get(sid) assert box is not None def _dead_run(coro, *, timeout=None): coro.close() raise RuntimeError("vsock disconnected") box._run = _dead_run output = box.execute_command("echo hi") assert output == "Error: vsock disconnected" assert box._closed is True assert provider.get(sid) is None provider.shutdown() def test_adopted_warm_pool_box_still_health_checks(monkeypatch): """Startup-adopted boxes must still pass a health check before reclaim.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config({"health_check_skip_seconds": 5}), ) provider = BoxliteProvider() adopted = BoxliteBox( "adopted", _FakeBox(name="deer-flow-boxlite-adopted"), _fake_run, default_env={}, ) provider._warm_pool["adopted"] = (adopted, time.time()) calls = 0 original_execute = adopted.execute_command def _record_health_check(command: str, *args, **kwargs): nonlocal calls calls += 1 return original_execute(command, *args, **kwargs) monkeypatch.setattr(adopted, "execute_command", _record_health_check) reclaimed = provider._reclaim_warm_pool( "adopted", ("some-user", "some-thread"), ) assert reclaimed == "adopted" assert calls == 1 assert "adopted" in provider._boxes assert "adopted" not in provider._warm_pool provider.shutdown() def test_dead_active_box_is_invalidated_after_command_failure(monkeypatch): monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config({"health_check_skip_seconds": 5}), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run sid = provider.acquire("thread-1", user_id="u1") box = provider.get(sid) assert box is not None def _dead_run(coro, *, timeout=None): coro.close() raise RuntimeError("vsock disconnected") box._run = _dead_run output = box.execute_command("echo hi") assert output == "Error: vsock disconnected" assert provider.get(sid) is None sid2 = provider.acquire("thread-1", user_id="u1") assert sid2 == sid assert provider.get(sid2) is not None provider.shutdown() def test_stale_closed_adapter_cannot_invalidate_recreated_box(monkeypatch): monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config({"health_check_skip_seconds": 5}), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run sid = provider.acquire("thread-1", user_id="u1") stale_box = provider.get(sid) assert stale_box is not None def _dead_run(coro, *, timeout=None): coro.close() raise RuntimeError("vsock disconnected") stale_box._run = _dead_run stale_box.execute_command("echo hi") assert provider.get(sid) is None provider._loop.run = _fake_run sid2 = provider.acquire("thread-1", user_id="u1") replacement = provider.get(sid2) assert sid2 == sid assert replacement is not None assert replacement is not stale_box stale_box.execute_command("echo again") assert provider.get(sid) is replacement assert replacement._closed is False provider.shutdown() def test_acquire_different_threads_dont_reclaim_each_other(monkeypatch): """Thread A's box can't be reclaimed by thread B.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run sid_a = provider.acquire("thread-a", user_id="u1") provider.release(sid_a) # Thread B acquires — should NOT get thread A's box sid_b = provider.acquire("thread-b", user_id="u1") assert sid_b != sid_a # Different deterministic ID assert sid_a in provider._warm_pool # A's box still in warm pool assert sid_b in provider._boxes # B's box is new def test_warm_pool_reclaim_failed_health_check_creates_new(monkeypatch): """Dead warm pool box is evicted and a new one created.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run sid1 = provider.acquire("thread-1", user_id="u1") provider.release(sid1) assert sid1 in provider._warm_pool # Corrupt the warm pool box: close it so health check fails box, _ = provider._warm_pool[sid1] box.close() # Stop VM, marks _closed=True # Re-acquire — health check should fail on the dead box # A new box is created with the same deterministic ID sid2 = provider.acquire("thread-1", user_id="u1") assert sid2 == sid1 # Same deterministic ID assert sid2 in provider._boxes replacement = provider.get(sid2) assert replacement is not None assert replacement is not box assert replacement._closed is False def test_concurrent_same_thread_acquire_creates_one_box(monkeypatch): """Concurrent acquires for one thread serialize before creating a named box.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run original_create_box = provider._create_box create_started = threading.Event() created: list[str] = [] def slow_create_box(sandbox_id: str) -> BoxliteBox: create_started.set() time.sleep(0.1) created.append(sandbox_id) return original_create_box(sandbox_id) provider._create_box = slow_create_box # type: ignore[method-assign] results: list[str] = [] def acquire() -> None: results.append(provider.acquire("thread-1", user_id="u1")) first = threading.Thread(target=acquire) second = threading.Thread(target=acquire) first.start() assert create_started.wait(timeout=2) second.start() first.join(timeout=2) second.join(timeout=2) assert len(results) == 2 assert results[0] == results[1] assert len(created) == 1 assert results[0] in provider._boxes provider.shutdown() def test_release_during_shutdown_closes_instead_of_reparking(monkeypatch): """release() must not park a VM after shutdown has begun.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run sid = provider.acquire("thread-1", user_id="u1") box = provider._boxes[sid] with provider._lock: provider._shutdown_called = True provider.release(sid) assert sid not in provider._boxes assert sid not in provider._warm_pool assert box._closed provider._loop.close() def test_reset_parks_running_resources_for_later_cleanup(monkeypatch): """reset() stops thread reuse but leaves VMs tracked for cleanup.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run sid_active = provider.acquire("thread-active", user_id="u1") sid_warm = provider.acquire("thread-warm", user_id="u1") provider.release(sid_warm) active_box = provider._boxes[sid_active] warm_box = provider._warm_pool[sid_warm][0] checker_thread = provider._idle_checker_thread loop_thread = provider._loop._thread provider.reset() assert provider._boxes == {} assert provider._warm_pool[sid_active][0] is active_box assert provider._warm_pool[sid_warm][0] is warm_box assert provider._thread_boxes == {} with pytest.raises(RuntimeError, match="closed"): with provider._acquire_serializer.hold("k"): pass assert not active_box._closed assert not warm_box._closed assert not provider._shutdown_called assert not provider._idle_checker_stop.is_set() assert checker_thread is not None assert checker_thread.is_alive() assert loop_thread.is_alive() provider.shutdown() assert active_box._closed assert warm_box._closed assert provider._idle_checker_stop.is_set() assert not checker_thread.is_alive() def test_reset_parked_resources_are_reaped_after_idle_timeout(monkeypatch): """VMs parked by reset remain visible to warm-pool idle cleanup.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run sid_active = provider.acquire("thread-active", user_id="u1") sid_warm = provider.acquire("thread-warm", user_id="u1") provider.release(sid_warm) active_box = provider._boxes[sid_active] warm_box = provider._warm_pool[sid_warm][0] provider.reset() provider._warm_pool[sid_active] = (active_box, time.time() - 9999) provider._warm_pool[sid_warm] = (warm_box, time.time() - 9999) provider._reap_expired_warm(idle_timeout=1) assert provider._warm_pool == {} assert active_box._closed assert warm_box._closed provider.shutdown() # ── Task 6: Idle reaper ─────────────────────────────────────────────── def test_idle_reaper_destroys_expired_warm_boxes(monkeypatch): """Idle reaper daemon destroys warm pool boxes that exceed the idle timeout.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) # Use a very short check interval so the reaper runs quickly monkeypatch.setattr(BoxliteProvider, "IDLE_CHECK_INTERVAL", 0.1) provider = BoxliteProvider() provider._loop.run = _fake_run # Acquire and release a box into the warm pool sid = provider.acquire("thread-1", user_id="u1") provider.release(sid) assert sid in provider._warm_pool # Backdate the warm-pool timestamp so it appears long-expired warm_box = provider._warm_pool[sid][0] provider._warm_pool[sid] = (warm_box, time.time() - 9999) # Wait long enough for the reaper to detect and destroy it time.sleep(0.3) # Box should be gone from warm pool and closed assert sid not in provider._warm_pool assert warm_box._closed provider.shutdown() # ── Task 7: Replica enforcement ─────────────────────────────────────── def test_replica_enforcement_evicts_oldest_warm(monkeypatch): """When warm pool exceeds replica limit, the oldest box is evicted.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config({"replicas": 2}), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run # Fill warm pool with 2 boxes from different threads sid_a = provider.acquire("thread-a", user_id="u1") provider.release(sid_a) sid_b = provider.acquire("thread-b", user_id="u1") provider.release(sid_b) assert len(provider._warm_pool) == 2 assert sid_a in provider._warm_pool assert sid_b in provider._warm_pool # Make sid_a definitely older by backdating its timestamp box_a = provider._warm_pool[sid_a][0] provider._warm_pool[sid_a] = (box_a, time.time() - 100) # Refresh sid_b's timestamp so it's newer box_b = provider._warm_pool[sid_b][0] provider._warm_pool[sid_b] = (box_b, time.time()) # Acquiring a third thread triggers replica enforcement: # warm pool count (2) >= replicas (2) → evict oldest (sid_a) sid_c = provider.acquire("thread-c", user_id="u1") # Oldest (sid_a) should be evicted (gone from warm pool, closed) assert sid_a not in provider._warm_pool assert box_a._closed # Newer (sid_b) should remain in warm pool assert sid_b in provider._warm_pool # New box (sid_c) should be active assert sid_c in provider._boxes assert sid_c not in provider._warm_pool provider.shutdown() def test_replica_enforcement_counts_active_and_warm(monkeypatch): """replicas caps active + warm boxes, not warm boxes alone.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config({"replicas": 2}), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run sid_active = provider.acquire("thread-active", user_id="u1") sid_warm = provider.acquire("thread-warm", user_id="u1") provider.release(sid_warm) warm_box = provider._warm_pool[sid_warm][0] sid_new = provider.acquire("thread-new", user_id="u1") assert sid_active in provider._boxes assert sid_new in provider._boxes assert sid_warm not in provider._warm_pool assert warm_box._closed provider.shutdown() # ── Task 8: Shutdown and reset including warm pool ──────────────────── def test_shutdown_stops_idle_reaper_and_destroys_all_boxes(monkeypatch): """shutdown stops the idle reaper thread and destroys all active + warm boxes.""" monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) provider = BoxliteProvider() provider._loop.run = _fake_run # Create one active box (thread-1) and one warm pool box (thread-2 released) sid_active = provider.acquire("thread-1", user_id="u1") sid_warm = provider.acquire("thread-2", user_id="u1") provider.release(sid_warm) assert sid_active in provider._boxes assert sid_warm in provider._warm_pool # Get box references before shutdown box_active = provider._boxes[sid_active] box_warm = provider._warm_pool[sid_warm][0] # Remember the idle checker thread checker_thread = provider._idle_checker_thread provider.shutdown() # Idle checker should be stopped assert provider._idle_checker_stop.is_set() assert checker_thread is not None assert not checker_thread.is_alive() # All boxes (active + warm) should be closed assert box_active._closed assert box_warm._closed # All collections should be empty assert len(provider._boxes) == 0 assert len(provider._warm_pool) == 0 assert len(provider._thread_boxes) == 0 def test_wider_id_separates_known_legacy_collision(): identity_a, identity_b = _LEGACY_COLLIDING_IDENTITIES user_a, thread_a = identity_a user_b, thread_b = identity_b old_a = hashlib.sha256(f"{user_a}:{thread_a}".encode()).hexdigest()[:8] old_b = hashlib.sha256(f"{user_b}:{thread_b}".encode()).hexdigest()[:8] assert old_a == old_b assert BoxliteProvider._sandbox_id(thread_a, user_a) != BoxliteProvider._sandbox_id( thread_b, user_b, ) def test_forced_collision_never_overwrites_active_tenant(monkeypatch): monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) monkeypatch.setattr( "deerflow.community.boxlite.provider._import_simplebox", lambda: _FakeBox, ) monkeypatch.setattr( BoxliteProvider, "_sandbox_id", staticmethod(lambda thread_id, user_id: "deadbeefdeadbeef"), ) provider = BoxliteProvider() provider._loop.run = _fake_run sandbox_id = provider.acquire("thread-a", user_id="user-a") box_a = provider.get(sandbox_id) provider.release(sandbox_id) assert box_a is not None assert sandbox_id in provider._warm_pool with pytest.raises(RuntimeError, match="Sandbox ID collision"): provider.acquire("thread-b", user_id="user-b") assert provider._warm_pool[sandbox_id][0] is box_a assert not box_a.is_closed assert provider.acquire("thread-a", user_id="user-a") == sandbox_id assert provider.get(sandbox_id) is box_a provider.shutdown() def test_late_same_tenant_collision_reuses_active_box(monkeypatch): monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) provider = BoxliteProvider() sandbox_id = "deadbeefdeadbeef" key = ("user-a", "thread-a") active = BoxliteBox( sandbox_id, _FakeBox(name=f"deer-flow-boxlite-{sandbox_id}"), _fake_run, default_env={}, ) duplicate = BoxliteBox( sandbox_id, _FakeBox(name=f"deer-flow-boxlite-{sandbox_id}"), _fake_run, default_env={}, ) monkeypatch.setattr( BoxliteProvider, "_sandbox_id", staticmethod(lambda thread_id, user_id: sandbox_id), ) def _create_box_with_late_registration(_sandbox_id): with provider._lock: provider._boxes[sandbox_id] = active provider._active_box_identity[sandbox_id] = key return duplicate monkeypatch.setattr(provider, "_create_box", _create_box_with_late_registration) assert provider.acquire("thread-a", user_id="user-a") == sandbox_id assert duplicate.is_closed assert provider.get(sandbox_id) is active assert provider._thread_boxes[key] == sandbox_id provider.shutdown() def test_failed_health_check_does_not_remove_swapped_warm_entry(monkeypatch): monkeypatch.setattr( "deerflow.community.boxlite.provider.get_app_config", lambda: _stub_config(), ) provider = BoxliteProvider() stale = BoxliteBox( "shared-id", _FakeBox(name="deer-flow-boxlite-shared-id"), _fake_run, default_env={}, ) replacement = BoxliteBox( "shared-id", _FakeBox(name="deer-flow-boxlite-shared-id"), _fake_run, default_env={}, ) provider._warm_pool["shared-id"] = (stale, time.time()) provider._warm_pool_identity["shared-id"] = ("user-a", "thread-a") def _swap_during_health_check(*args, **kwargs): with provider._lock: provider._warm_pool["shared-id"] = (replacement, time.time()) provider._warm_pool_identity["shared-id"] = ( "user-b", "thread-b", ) return "not healthy" monkeypatch.setattr(stale, "execute_command", _swap_during_health_check) with pytest.raises(RuntimeError, match="Sandbox ID collision"): provider._reclaim_warm_pool( "shared-id", ("user-a", "thread-a"), ) assert provider._warm_pool["shared-id"][0] is replacement assert provider._warm_pool_identity["shared-id"] == ( "user-b", "thread-b", ) assert not replacement.is_closed provider.shutdown() def test_sandbox_id_matches_shared_identity(): from deerflow.sandbox.identity import derive_sandbox_scope_token assert BoxliteProvider._sandbox_id("t-1", "u-1") == derive_sandbox_scope_token(user_id="u-1", thread_id="t-1") def test_sandbox_id_none_user_quirk_pinned(): """BoxLite passes user_id through raw; None renders as the literal "None". Quirk pinned per RFC #4741 §2.2 (its _thread_key uses "" instead, so the two disagree). NOT fixed here — unifying the resolution is a separate behavior-changing decision with its own follow-up issue. """ from deerflow.sandbox.identity import derive_sandbox_scope_token assert BoxliteProvider._sandbox_id("t-1", None) == derive_sandbox_scope_token(user_id="None", thread_id="t-1") def test_list_dir_and_glob_preserve_trailing_space_in_filename() -> None: # "notes.txt " (trailing space) is a legal Linux filename; find prints it # verbatim, one entry per line, so a per-line strip() corrupts the name. class _FindBox: async def exec(self, *argv, env=None, timeout=None): marker = "__DF_SEARCH_STATUS__" if "__DF_SEARCH_STATUS__:" in argv[2] else "__DF_FIND_STATUS__" return types.SimpleNamespace(stdout=f"/mnt/user-data/workspace/notes.txt \n\n{marker}:0\n", stderr="", exit_code=0) box = BoxliteBox("box-id", box=_FindBox(), run=_fake_run) assert box.list_dir("/mnt/user-data/workspace") == ["/mnt/user-data/workspace/notes.txt "] found, truncated = box.glob("/mnt/user-data/workspace", "notes*") assert found == ["/mnt/user-data/workspace/notes.txt "] assert truncated is False def test_list_dir_raises_when_find_returns_no_entries() -> None: class _EmptyBox: async def exec(self, *argv, env=None, timeout=None): return types.SimpleNamespace(stdout="\n__DF_FIND_STATUS__:1\n", stderr="", exit_code=1) box = BoxliteBox("box-id", box=_EmptyBox(), run=_fake_run) with pytest.raises(FileNotFoundError): box.list_dir("/mnt/user-data/workspace") def test_list_dir_raises_oserror_when_find_exit_is_not_missing_path() -> None: # find exit 1 is "start point absent"; 127 (no binary) must not look missing. class _MissingBinaryBox: async def exec(self, *argv, env=None, timeout=None): return types.SimpleNamespace(stdout="", stderr="", exit_code=127) box = BoxliteBox("box-id", box=_MissingBinaryBox(), run=_fake_run) with pytest.raises(OSError, match="exited with code 127"): box.list_dir("/mnt/user-data/workspace") def test_list_dir_uses_find_H_to_dereference_start_point() -> None: captured: list[tuple] = [] class _FindBox: async def exec(self, *argv, env=None, timeout=None): captured.append(argv) return types.SimpleNamespace(stdout="/mnt/user-data/workspace\n\n__DF_FIND_STATUS__:0\n", stderr="", exit_code=0) box = BoxliteBox("box-id", box=_FindBox(), run=_fake_run) assert box.list_dir("/mnt/user-data/workspace") == ["/mnt/user-data/workspace"] assert any(len(argv) >= 3 and "find -H " in str(argv[2]) for argv in captured) # ── Remote grep/glob failure contract against a real POSIX sh (#5376) ───────── _RS_POSIX = pytest.mark.skipif( os.name == "nt" or any(shutil.which(tool) is None for tool in ("sh", "head", "grep", "find")), reason="POSIX sh, head, grep and find required", ) def _rs_env(tmp_path, failing: str | None = None) -> dict[str, str]: env = os.environ.copy() if failing is not None: bin_dir = tmp_path / "fake-bin" bin_dir.mkdir() fake = bin_dir / failing fake.write_text("#!/bin/sh\nexit 127\n", encoding="utf-8") fake.chmod(0o755) env["PATH"] = str(bin_dir) + os.pathsep + env.get("PATH", "") return env def _rs_box(tmp_path, monkeypatch, failing: str | None = None) -> BoxliteBox: box = BoxliteBox("box-id", box=_FakeBox(name="box-id"), run=_fake_run) shell_env = _rs_env(tmp_path, failing) def sh(script: str, env=None, timeout=None): # ``sh -c`` (not ``-lc``) keeps a login profile from overriding the fake PATH. proc = subprocess.run(["sh", "-c", script], capture_output=True, text=True, env=shell_env, check=False) return types.SimpleNamespace(stdout=proc.stdout, stderr=proc.stderr, exit_code=proc.returncode) monkeypatch.setattr(box, "_sh", sh) return box def _rs_search(box, op: str, root: str): return box.grep(root, "needle") if op == "grep" else box.glob(root, "**/*.py") @_RS_POSIX @pytest.mark.parametrize("op", ["grep", "glob"]) def test_remote_search_missing_root_raises_file_not_found(tmp_path, monkeypatch, op) -> None: with pytest.raises(FileNotFoundError): _rs_search(_rs_box(tmp_path, monkeypatch), op, str(tmp_path / "missing")) @_RS_POSIX @pytest.mark.parametrize(("op", "binary"), [("grep", "grep"), ("glob", "find")]) def test_remote_search_missing_binary_raises_instead_of_no_matches(tmp_path, monkeypatch, op, binary) -> None: with pytest.raises(OSError, match="exited with code 127"): _rs_search(_rs_box(tmp_path, monkeypatch, failing=binary), op, str(tmp_path)) @_RS_POSIX def test_remote_search_keeps_real_matches_and_genuine_no_match(tmp_path, monkeypatch) -> None: (tmp_path / "src").mkdir() (tmp_path / "src" / "app.py").write_text("def needle():\n", encoding="utf-8") box = _rs_box(tmp_path, monkeypatch) matches, _ = box.grep(str(tmp_path), "needle") assert [(os.path.basename(m.path), m.line_number) for m in matches] == [("app.py", 1)] assert box.grep(str(tmp_path), "zzz_nothing") == ([], False) found, _ = box.glob(str(tmp_path), "**/*.py") assert [os.path.basename(path) for path in found] == ["app.py"] assert box.glob(str(tmp_path), "*.md") == ([], False)