Fix Windows backend test compatibility (#1384)

* 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>
This commit is contained in:
Admire
2026-03-26 17:39:16 +08:00
committed by GitHub
parent b3d3287b80
commit b9583f7204
10 changed files with 141 additions and 27 deletions

View File

@@ -3,6 +3,8 @@
import importlib
from unittest.mock import MagicMock, patch
import pytest
from deerflow.config.paths import Paths
# ── ensure_thread_dirs ───────────────────────────────────────────────────────
@@ -71,3 +73,33 @@ def test_get_thread_mounts_includes_user_data_dirs(tmp_path, monkeypatch):
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 == []

View File

@@ -2146,7 +2146,12 @@ class TestUploadDeleteSymlink:
# Create a symlink inside uploads dir pointing to outside file.
link = uploads_dir / "harmless.txt"
link.symlink_to(outside)
try:
link.symlink_to(outside)
except OSError as exc:
if getattr(exc, "winerror", None) == 1314:
pytest.skip("symlink creation requires Developer Mode or elevated privileges on Windows")
raise
with patch("deerflow.client.get_uploads_dir", return_value=uploads_dir), patch("deerflow.client.ensure_uploads_dir", return_value=uploads_dir):
# The resolved path of the symlink escapes uploads_dir,

View File

@@ -5,9 +5,16 @@ from __future__ import annotations
import subprocess
import tempfile
from pathlib import Path
from shutil import which
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
SCRIPT_PATH = REPO_ROOT / "scripts" / "docker.sh"
BASH_EXECUTABLE = which("bash") or r"C:\Program Files\Git\bin\bash.exe"
if not Path(BASH_EXECUTABLE).exists():
pytestmark = pytest.mark.skip(reason="bash is required for docker.sh detection tests")
def _detect_mode_with_config(config_content: str) -> str:
@@ -19,7 +26,7 @@ def _detect_mode_with_config(config_content: str) -> str:
command = f"source '{SCRIPT_PATH}' && PROJECT_ROOT='{tmp_root}' && detect_sandbox_mode"
output = subprocess.check_output(
["bash", "-lc", command],
[BASH_EXECUTABLE, "-lc", command],
text=True,
).strip()
@@ -30,7 +37,7 @@ def test_detect_mode_defaults_to_local_when_config_missing():
"""No config file should default to local mode."""
with tempfile.TemporaryDirectory() as tmpdir:
command = f"source '{SCRIPT_PATH}' && PROJECT_ROOT='{tmpdir}' && detect_sandbox_mode"
output = subprocess.check_output(["bash", "-lc", command], text=True).strip()
output = subprocess.check_output([BASH_EXECUTABLE, "-lc", command], text=True).strip()
assert output == "local"

View File

@@ -25,6 +25,10 @@ class TestIsUnsafeZipMember:
info = zipfile.ZipInfo("/etc/passwd")
assert is_unsafe_zip_member(info) is True
def test_windows_absolute_path(self):
info = zipfile.ZipInfo("C:\\Windows\\system32\\drivers\\etc\\hosts")
assert is_unsafe_zip_member(info) is True
def test_dotdot_traversal(self):
info = zipfile.ZipInfo("foo/../../../etc/passwd")
assert is_unsafe_zip_member(info) is True

View File

@@ -4,6 +4,10 @@ from langgraph.runtime import Runtime
from deerflow.agents.middlewares.thread_data_middleware import ThreadDataMiddleware
def _as_posix(path: str) -> str:
return path.replace("\\", "/")
class TestThreadDataMiddleware:
def test_before_agent_returns_paths_when_thread_id_present_in_context(self, tmp_path):
middleware = ThreadDataMiddleware(base_dir=str(tmp_path), lazy_init=True)
@@ -11,9 +15,9 @@ class TestThreadDataMiddleware:
result = middleware.before_agent(state={}, runtime=Runtime(context={"thread_id": "thread-123"}))
assert result is not None
assert result["thread_data"]["workspace_path"].endswith("threads/thread-123/user-data/workspace")
assert result["thread_data"]["uploads_path"].endswith("threads/thread-123/user-data/uploads")
assert result["thread_data"]["outputs_path"].endswith("threads/thread-123/user-data/outputs")
assert _as_posix(result["thread_data"]["workspace_path"]).endswith("threads/thread-123/user-data/workspace")
assert _as_posix(result["thread_data"]["uploads_path"]).endswith("threads/thread-123/user-data/uploads")
assert _as_posix(result["thread_data"]["outputs_path"]).endswith("threads/thread-123/user-data/outputs")
def test_before_agent_uses_thread_id_from_configurable_when_context_is_none(self, tmp_path, monkeypatch):
middleware = ThreadDataMiddleware(base_dir=str(tmp_path), lazy_init=True)
@@ -26,7 +30,7 @@ class TestThreadDataMiddleware:
result = middleware.before_agent(state={}, runtime=runtime)
assert result is not None
assert result["thread_data"]["workspace_path"].endswith("threads/thread-from-config/user-data/workspace")
assert _as_posix(result["thread_data"]["workspace_path"]).endswith("threads/thread-from-config/user-data/workspace")
assert runtime.context is None
def test_before_agent_uses_thread_id_from_configurable_when_context_missing_thread_id(self, tmp_path, monkeypatch):
@@ -40,7 +44,7 @@ class TestThreadDataMiddleware:
result = middleware.before_agent(state={}, runtime=runtime)
assert result is not None
assert result["thread_data"]["uploads_path"].endswith("threads/thread-from-config/user-data/uploads")
assert _as_posix(result["thread_data"]["uploads_path"]).endswith("threads/thread-from-config/user-data/uploads")
assert runtime.context == {}
def test_before_agent_raises_clear_error_when_thread_id_missing_everywhere(self, tmp_path, monkeypatch):

View File

@@ -87,7 +87,12 @@ class TestValidatePathTraversal:
target = tmp_path.parent / "secret.txt"
target.touch()
link = tmp_path / "escape"
link.symlink_to(target)
try:
link.symlink_to(target)
except OSError as exc:
if getattr(exc, "winerror", None) == 1314:
pytest.skip("symlink creation requires Developer Mode or elevated privileges on Windows")
raise
with pytest.raises(PathTraversalError, match="traversal"):
validate_path_traversal(link, tmp_path)