diff --git a/backend/tests/test_channel_file_attachments.py b/backend/tests/test_channel_file_attachments.py index bf5395a52c..0566a94a64 100644 --- a/backend/tests/test_channel_file_attachments.py +++ b/backend/tests/test_channel_file_attachments.py @@ -585,6 +585,7 @@ class TestInboundFileSandboxPerms: # Best-effort helper: a missing file is a silent no-op. _make_inbound_file_sandbox_readable(tmp_path / "does-not-exist.txt") + @pytest.mark.skipif(os.name != "posix", reason="POSIX-only: Windows CRT chmod cannot express the 0600 mode this test asserts") def test_make_inbound_file_sandbox_readable_swap_after_lstat_does_not_follow_symlink(self, tmp_path, monkeypatch): from app.channels.manager import _make_inbound_file_sandbox_readable @@ -614,6 +615,12 @@ class TestInboundFileSandboxPerms: os.symlink(target, path) return st + # Staging the TOCTOU swap needs symlink creation; a host without the + # privilege cannot stage the race at all, so skip before installing the hook. + probe = tmp_path / ".symlink_probe" + symlink_or_skip(probe, upload) + probe.unlink() + monkeypatch.setattr(os, "lstat", racing_lstat) _make_inbound_file_sandbox_readable(upload) diff --git a/backend/tests/test_review_skill_package_tool.py b/backend/tests/test_review_skill_package_tool.py index 59bd31239b..b5fdd5b330 100644 --- a/backend/tests/test_review_skill_package_tool.py +++ b/backend/tests/test_review_skill_package_tool.py @@ -4,6 +4,7 @@ from pathlib import Path from types import SimpleNamespace import pytest +from support.symlinks import symlink_or_skip from deerflow.config.paths import Paths, make_safe_user_id from deerflow.skills.storage.local_skill_storage import LocalSkillStorage @@ -195,7 +196,7 @@ def test_review_skill_package_rejects_symlink_planted_in_callers_outputs(deploym outputs_dir = deployment.sandbox_outputs_dir("thread-1", user_id="alice") outputs_dir.mkdir(parents=True) link = outputs_dir / "borrowed-skill" - link.symlink_to(victim_package, target_is_directory=True) + symlink_or_skip(link, victim_package, target_is_directory=True) command = review_skill_package.func( target=str(link), diff --git a/backend/tests/test_shared_reset_marker.py b/backend/tests/test_shared_reset_marker.py index ad6de7bfaa..7237677748 100644 --- a/backend/tests/test_shared_reset_marker.py +++ b/backend/tests/test_shared_reset_marker.py @@ -15,6 +15,7 @@ import json from pathlib import Path import pytest +from support.symlinks import symlink_or_skip import deerflow.config.shared_reset_marker as shared_reset_marker_module from deerflow.config.extensions_config import ExtensionsConfig @@ -58,7 +59,7 @@ def test_marker_path_is_hidden_sibling_of_the_config(config_path: Path) -> None: def test_marker_path_follows_a_symlinked_config(tmp_path: Path, config_path: Path) -> None: link = tmp_path / "linked" / "extensions_config.json" link.parent.mkdir() - link.symlink_to(config_path) + symlink_or_skip(link, config_path) assert SharedResetMarker("skills-cache-reset").path_for(link) == config_path.parent / ".extensions_config.json.skills-cache-reset.json" diff --git a/backend/tests/test_view_image_middleware.py b/backend/tests/test_view_image_middleware.py index d01eeeec8f..e6e5c0657c 100644 --- a/backend/tests/test_view_image_middleware.py +++ b/backend/tests/test_view_image_middleware.py @@ -33,6 +33,7 @@ from langchain.agents.middleware.types import ModelRequest from langchain_core.callbacks import BaseCallbackHandler from langchain_core.language_models.fake_chat_models import FakeMessagesListChatModel from langchain_core.messages import AIMessage, AnyMessage, HumanMessage, SystemMessage, ToolMessage +from support.symlinks import symlink_or_skip from deerflow.agents.middlewares.view_image_middleware import ( _IMAGE_CONTEXT_MESSAGE_MARKER_KEY, @@ -902,7 +903,7 @@ def test_host_image_copy_is_scoped_to_current_user_and_thread(tmp_path, monkeypa alias_path = paths.sandbox_outputs_dir("thread-a", user_id="user-a") / "canary.png" alias_path.parent.mkdir(parents=True) - alias_path.symlink_to(owner_path) + symlink_or_skip(alias_path, owner_path) assert model_payloads("user-a", "thread-a", alias_path) == []