Thanks to visit codestin.com
Credit goes to github.com

Skip to content

test(build-context): skip symlink cases where the platform refuses symlinks - #501

Merged
rng1995 merged 4 commits into
NVIDIA:mainfrom
kevin9327:fix/build-context-symlink-skip-guard
Sep 12, 2026
Merged

test(build-context): skip symlink cases where the platform refuses symlinks#501
rng1995 merged 4 commits into
NVIDIA:mainfrom
kevin9327:fix/build-context-symlink-skip-guard

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

Problem

Five symlink-rejection cases in tests/nodes/test_build_context.py build their fixture with
Path.symlink_to and let any OSError escape:

(skill_dir / "creds.md").symlink_to(secret)

Creating a symlink is a privileged operation on Windows unless Developer Mode is on or the
process holds SeCreateSymbolicLinkPrivilege. Where it is refused, the error is raised during
fixture setup, so the case reports as a failure rather than as a case that could not run. The
assertions it exists to make — that a symlinked component never reaches components, never
reaches file_cache, and never leaks its target's content — are never evaluated, and the
failure looks identical to a real regression in that rejection logic.

This is a gap against a guard the suite already applies everywhere else. Every other module
that builds a symlink fixture wraps the creation call:

tests/nodes/test_resolve_input.py:53      pytest.skip("symlinks are not supported on this filesystem")
tests/test_multi_skill.py:309,325,477     pytest.skip("symlinks are not supported on this filesystem")
tests/unit/test_cli.py:132,3514           pytest.skip("symlinks are not supported on this filesystem")
tests/unit/test_input_handler.py:102,122,142,212
                                          pytest.skip("symlinks are not supported on this filesystem")

tests/nodes/test_build_context.py applies the same guard to its dangling-symlink case
(test_build_context_excludes_dangling_symlink_from_scan_scope) and omits it from the other
five.

Fix

Wrap the five unguarded symlink_to calls in the guard this file already uses, with the same
wording as the existing one. test_build_context_rejects_file_swapped_to_symlink_before_read
creates its symlink inside a monkeypatched open, where build_context catches OSError and
records read_error instead of the expected not_regular_file; that case gets an equivalent
up-front probe so the reason is "cannot run here" rather than a wrong reason code.

The except OSError wraps only the symlink_to call, so on any platform that can create
symlinks — including ubuntu-latest, which is what CI runs — every assertion executes exactly
as before. Nothing is skipped there.

Reproduction

Windows 11, Python 3.12.10, symlink creation not permitted for the process. Run against
unmodified main (704bc95):

$ python -m pytest -p no:randomly tests/nodes/test_build_context.py \
    -k "rejects_symlink_to_external_file or rejects_symlinked_directory or \
        rejects_in_tree_symlink or rejects_file_swapped_to_symlink_before_read or \
        rejects_symlinked_manifest" -q --no-header

    def symlink_to(self, target, target_is_directory=False):
        if not hasattr(os, "symlink"):
            raise NotImplementedError("os.symlink() not available on this system")
>       os.symlink(target, self, target_is_directory)
E       OSError: [WinError 1314] <localized ERROR_PRIVILEGE_NOT_HELD text>:
        '...\\external_manifest.md' -> '...\\skill\\SKILL.md'

..\Python312\Lib\pathlib.py:1386: OSError
=========================== short test summary info ===========================
FAILED tests/nodes/test_build_context.py::test_build_context_rejects_symlink_to_external_file
FAILED tests/nodes/test_build_context.py::test_build_context_rejects_symlinked_directory
FAILED tests/nodes/test_build_context.py::test_build_context_rejects_in_tree_symlink
FAILED tests/nodes/test_build_context.py::test_build_context_rejects_file_swapped_to_symlink_before_read
FAILED tests/nodes/test_build_context.py::test_build_context_rejects_symlinked_manifest
5 failed, 75 deselected in 22.91s

WinError 1314 is ERROR_PRIVILEGE_NOT_HELD; the message text itself is locale-dependent and
absolute paths are elided above.

With this change, same command:

sssss                                                                    [100%]
=========================== short test summary info ===========================
SKIPPED [1] tests\nodes\test_build_context.py:1081: symlinks are unavailable on this platform
SKIPPED [1] tests\nodes\test_build_context.py:1102: symlinks are unavailable on this platform
SKIPPED [1] tests\nodes\test_build_context.py:1140: symlinks are unavailable on this platform
SKIPPED [1] tests\nodes\test_build_context.py:1162: symlinks are unavailable on this platform
SKIPPED [1] tests\nodes\test_build_context.py:1194: symlinks are unavailable on this platform
5 skipped, 75 deselected in 3.81s

What must still hold

tests/nodes/test_build_context.py as a whole, before and after — the count moves by exactly
these five and nothing else changes state:

failed passed skipped
before 12 66 2
after 7 66 7

The seven that remain are unrelated to symlinks and are not touched here.

ruff check src/ tests/All checks passed!
ruff format --check src/ tests/223 files already formatted

…mlinks

The symlink rejection tests create their fixture with Path.symlink_to and let
any OSError escape. On a platform that refuses symlink creation the error is
raised during setup, so the test errors instead of reporting that the case
could not run.

Every other module that builds symlink fixtures already guards the creation
call, and this file itself guards the dangling-symlink case the same way.
Apply that guard to the five cases that were missed.

Signed-off-by: kevin9327 <[email protected]>
@MohammedAlkindi

Copy link
Copy Markdown

Verified on Windows 11 without the symlink-creation privilege, which is the configuration this guards.

tests/nodes/test_build_context.py, same venv, -p no:randomly:

ref failed passed skipped
main 69dcdfb 12 66 2
+ this PR 8 65 7

The five it resolves are exactly the ones that cannot run here:

test_build_context_rejects_in_tree_symlink
test_build_context_rejects_symlinked_directory
test_build_context_rejects_symlinked_manifest
test_build_context_rejects_symlink_to_external_file
test_build_context_rejects_file_swapped_to_symlink_before_read

All five fail on main with the same error, for example:

OSError: [WinError 1314] A required privilege is not held by the client:
  ...\test_build_context_rejects_in_0\skill\real.md -> ...\skill\alias.md

They become symlinks are unavailable on this platform skips, which is the right outcome: creating a symlink on Windows needs elevation or Developer Mode, so those cases were never exercising the code under test here.

One thing to discount if you see it. test_dense_directory_discovery_and_cache_complete_with_modest_real_elapsed_time appeared to be a new failure in my first comparison run. It is not: run three times per ref it fails 1 of 3 on main (28.5s) and passes 3 of 3 with this PR (6.0s, 8.4s, 7.0s). It is a pre-existing timing-sensitive test and my first sample caught it under load.

The remaining 8 failures are unrelated to this change and reproduce identically on main.

SanHsien added a commit to SanHsien/SkillSpector that referenced this pull request Sep 11, 2026
…VIDIA#524

Raise reviewed_pr_through to 527 and reviewed_issue_through to 524 in
tools/upstream_baseline.json (commit axis unchanged at 69dcdfb). Every
item gets a verdict in docs/DECISIONS.md: NVIDIA#493/NVIDIA#507/NVIDIA#508/NVIDIA#511 verified
via git merge-base --is-ancestor as already included through the
2.11.1/2.11.2 sync (including NVIDIA#521, which merged only into the still-
open NVIDIA#516 stack, not main); the remaining 27 items stay "wait for
upstream merge", none adopted now.

Two items get dedicated comparison notes per docs/DIVERGENCE.md's
static_runner.py and scripts/compare_scan_accuracy.py rows: NVIDIA#522 uses a
different env var name and different default/semantics than this
fork's SKILLSPECTOR_MAX_STATIC_SECONDS, so merging it cannot simply
delete the divergence row and needs a downstream env var migration
first; NVIDIA#490 extends this fork's own upstream PR NVIDIA#486 with a Python
3.14/POSIX edge case the fork's Windows environment does not hit, so
NVIDIA#486 is left untouched pending upstream's own resolution. NVIDIA#501-NVIDIA#505 and
NVIDIA#518 are also flagged as near-verbatim matches to this fork's existing
Windows test divergence rows, worth revisiting for row deletion once
merged.

Co-Authored-By: Claude Opus 5 <[email protected]>
Signed-off-by: SanHsien <[email protected]>

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SkillSpector Review]

Approved at head f7c24183bc34266defb20af8f2833edaec654251.

The tests now skip only when the platform actually refuses symlink creation, while continuing to exercise all original assertions when symlinks are available. The same-directory probe avoids cross-volume privilege differences, and required CI is green.

Merge gate: the PR is mergeable but BEHIND; update it to the protected branch's required head state before merging.

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SkillSpector Review]

Re-reviewed current head c9811c218e94a896e9096259c2c36869df8258c1 after the later main synchronizations. The sole PR-owned test blob is unchanged. Each guard remains scoped to fixture symlink creation, while all original security assertions still execute on platforms that support symlinks; the swap test retains its same-directory capability probe. I found no required change.

Merge gate: test-unit is still running on this head and GitHub reports mergeStateStatus=BLOCKED; wait for required checks to pass before merging.

@rng1995
rng1995 enabled auto-merge (squash) September 12, 2026 19:13
@rng1995
rng1995 merged commit 94a34a2 into NVIDIA:main Sep 12, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants