fix(cache): keep Windows probes out of concurrently scanned trees - #1795
Conversation
Nightly CI failed on windows-latest/3.11 for three consecutive runs with `has_errors is True` from the assets-tree scans in tests/test_security_asset_integration.py. Root cause: on Windows a cache identity probe is a TemporaryFile whose name stays visible until close, so it is observable to any scan walking the directory that holds it. `_get_change_clock_probe` only reaches the ancestor fallback when the temp and cache directories are on another volume — exactly the Windows CI layout, where the checkout is on D: while %TEMP% and ~/.modelaudit are on C:. It then walked `protected_root.parents` nearest-first, so the probe landed just above the scanned file, inside tests/assets. A concurrent xdist worker scanning tests/assets enumerated the probe, and `os.path.getsize` raised once the probe was released, producing `operational_error_reason: file_size_check_failed` and failing the scan. The 7/26 nightly log captured the probe under tests/assets/samples/pickles/. Walk the ancestors outermost-first so the fallback settles near the volume root rather than inside a deep tree an unrelated scan is enumerating. The ordering change is confined to the Windows branch; POSIX TemporaryFile unlinks immediately and is never visible to a directory walk. Also report the offending paths and reasons in the asset-scan assertions, which previously truncated to an unhelpful model repr and hid which file actually failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Performance BenchmarksCompared
|
There was a problem hiding this comment.
Pull request overview
This PR fixes a Windows-specific cache/probe placement bug where cross-volume cache identity probes could be created inside a concurrently scanned directory tree, causing intermittent operational errors in parallel test runs. It also improves integration-test assertion diagnostics and adds a regression test to reproduce the CI topology.
Changes:
- Adjust Windows
_get_change_clock_probecandidate ordering to prefer outermost ancestors when falling back to scanned-path ancestors. - Add a targeted regression test covering cross-volume fallback probe placement outside the scanned assets tree.
- Improve asset integration test assertions by including per-file operational error details in failure messages; update changelog.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
modelaudit/cache/scan_results_cache.py |
Changes Windows probe candidate ordering to avoid placing temp probes inside deep scanned trees. |
tests/cache/test_cache_correctness.py |
Adds regression coverage for cross-volume Windows probe placement behavior. |
tests/test_security_asset_integration.py |
Makes has_errors assertions self-diagnosing by summarizing per-file operational error metadata. |
CHANGELOG.md |
Documents the Windows probe placement behavior change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Windows keeps TemporaryFile names visible and locked until close, so a | ||
| # probe is observable to every concurrent scan that walks the directory | ||
| # holding it. Ancestors are only reached when the temp and cache | ||
| # directories live on another volume; walk them outermost-first so the | ||
| # fallback settles near the volume root instead of inside a deep tree | ||
| # that an unrelated scan is enumerating. |
Why
Nightly CI failed on
windows-latest/ Python 3.11 for three consecutive runs (Jul 29, 30, 31), plus Jul 26 and Jul 22. The recurring failure is the assets-tree scan intests/test_security_asset_integration.py:Root cause
On Windows a cache identity probe is a
TemporaryFilewhose name stays visible until close, so it is observable to any scan walking the directory that holds it. (On POSIX the file is unlinked immediately and never appears in a directory walk — which is why this is Windows-only.)_get_change_clock_probeonly reaches its ancestor fallback when the temp and cache directories are on a different volume from the scanned file — exactly the Windows CI layout, where the checkout is onD:while%TEMP%and~/.modelauditare onC:. Both preferred candidates fail the same-device check, and the code then walkedprotected_root.parentsnearest-first, so the probe landed just above the scanned file — insidetests/assets.A concurrent
pytest-xdistworker scanningtests/assetsthen enumerated that probe, andos.path.getsizeraised once the probe was released, yieldingoperational_error_reason: file_size_check_failed→has_errors=True→ whole scan fails.The Jul 26 nightly log captured the probe directly:
This is a real product defect beyond CI: modelaudit could write temporary probe files into a user's model directory whenever the scan target is on a different volume from temp/cache (e.g. a mounted model volume on Windows).
Fix
Walk the ancestors outermost-first so the cross-volume fallback settles near the volume root instead of inside a deep tree an unrelated scan is enumerating. The change is confined to the
os.name == "nt"branch; POSIX ordering is untouched and correct as-is.Also made the asset-scan assertions self-diagnosing — they previously truncated to an unhelpful model repr that hid which file failed, which is why this took several nightlies to pin down.
Tests
test_windows_change_clock_probe_prefers_outermost_ancestor_over_scanned_treereproduces the CI topology (off-device temp/cache, every ancestor on-device). It fails on the pre-fix code with the probe landing in.../tests/assets/samples— the exact CI signature — and passes after.Validation
ruff format/ruff check: cleanmypy: clean (only 5 pre-existing macOS-onlyos.setxattr/getxattrerrors, present onmain)pytest -n auto -m "not slow and not integration": 81 failures on this branch vs 84 on cleanmainin the same local macOS environment; 70 shared. The 11 branch-only entries all pass in isolation (xdist-load flakes in the cache-invalidation family, churning in both directions — 14 tests failed only onmain). No regressions.Known remaining nightly flakes (not addressed here)
Two other Windows-only flake classes appeared in the same nightlies and are separate root causes:
packages/modelaudit-picklescan/tests/test_api.py—ScanStatus.INCONCLUSIVEinstead ofCOMPLETEfrom the shared call-graph source-stability guard (Jul 26, Jul 29). fix(cache): isolate Windows probes and stabilize nightly checks #1782 established a narrow tolerance helper (_assert_call_graph_source_stability_error) for this; extending it needs a real Windows reproduction to know which assertions still hold underINCONCLUSIVE, so it is deliberately not guessed at here.tests/utils/file/test_large_file_handler.py::test_large_handler_cache_preserves_private_metadata_for_internal_results—assert 2 == 1, a cache-store decline under load (Jul 30). Same family as the locally-flaky cache tests above.🤖 Generated with Claude Code