test: route four unguarded symlink fixtures through symlink_or_skip (#6619)

* test: route four unguarded symlink fixtures through symlink_or_skip

`tests/support/symlinks.py` was added in #5414 so suites that need a real
symlink skip cleanly when the host cannot create one. On a stock Windows host
(no Developer Mode, not elevated) `Path.symlink_to` / `os.symlink` raise
`OSError: [WinError 1314]`, and four fixtures still called them directly, so
they reported hard errors instead of skipping:

- tests/test_shared_reset_marker.py::test_marker_path_follows_a_symlinked_config
- tests/test_view_image_middleware.py::test_host_image_copy_is_scoped_to_current_user_and_thread
- tests/test_review_skill_package_tool.py::test_review_skill_package_rejects_symlink_planted_in_callers_outputs
- tests/test_channel_file_attachments.py::TestInboundFileSandboxPerms::test_make_inbound_file_sandbox_readable_swap_after_lstat_does_not_follow_symlink

These were the only symlink-related failures in a Windows run of the full
backend suite; the other bare symlink call sites there are already guarded by a
`skipif` or a `try/except OSError`. The view-image test plants its symlink
after its scoping assertions, so those still run before the skip. The
inbound-attachment test stages a symlink from inside a monkeypatched `os.lstat`
to exercise a TOCTOU race; a host that cannot create a symlink cannot stage
that race at all, so skipping is the only honest outcome.

* test: make channel symlink race fixture portable

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Jason Cat 2026-10-10 20:52:17 +08:00 committed by GitHub
parent db7340d2fa
commit d37616e659
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 13 additions and 3 deletions

View File

@ -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)

View File

@ -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),

View File

@ -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"

View File

@ -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) == []