Skip to content

doctor.py: six more is_dir()/exists() swallow sites, the class #341 closed one instance of - #371

Merged
fdaviddpt merged 3 commits into
mainfrom
fix/363
Aug 20, 2026
Merged

doctor.py: six more is_dir()/exists() swallow sites, the class #341 closed one instance of#371
fdaviddpt merged 3 commits into
mainfrom
fix/363

Conversation

@fdaviddpt

Copy link
Copy Markdown
Contributor

An unreadable parent directory made check_directory, check_jit_rules and resolve_project_dir's own --root check swallow to a confident "does not exist"/"no rules for this repo", with a remedy telling the reader to create something that may already be there -- #341's own sentence one call site over, six more times (filed as #363's sweep). Fixes #363.

What changed

  • Added _safe_is_dir(path) (swallow-only, mirrors the existing _safe_is_file) for callers that only filter a list, and _dir_state(path) -- a genuine three-state classifier (dir/absent/unreadable) for callers that print a verdict -- near the existing _safe_is_file helper.
  • check_directory, check_jit_rules, and resolve_project_dir's own chosen check (the entry point where doctor's third-state contract is established, not merely consumed) now use _dir_state instead of a bare is_dir(), replacing a confident absence with a distinct "could not be checked" WARN.
  • resolve_project_dir's .git existence check is now wrapped in its own try/except OSError.
  • merge_permission_state's path.exists() now routes an unreadable settings candidate into the unknown bucket the function already had and never reached.
  • The two narrow glob-filter comprehensions (jit_hook_roots, _jit_layer_verdict) now use _safe_is_dir so one unreadable candidate no longer wipes every candidate already found via the outer except OSError.

The self-review round that mattered

The first version of _dir_state classified "unreadable" by catching OSError out of Path.is_dir(). A spawned audit (second pass, after the first pass returned a header-less, partially-unstated message and was re-run once per the developer brief) measured directly that on this machine's local Python 3.14, Path.is_dir() itself swallows the PermissionError an unreadable parent produces and returns False -- so the except OSError branch was unreachable on precisely the interpreter CLAUDE.md already names as the one that swallows, and the fix would have silently degraded back into the exact bug #363 exists to close, invisible to CI (which gates 3.9-3.12 only). _dir_state now calls path.stat() instead (measured to raise on the same fixture on 3.14), with FileNotFoundError/NotADirectoryError caught ahead of the general OSError arm so a genuinely absent path isn't folded into "unreadable" -- a regression the first attempt at this fix introduced and this file's own must-not-fire controls caught before commit. The real chmod-based test in the suite, which had been silently self-skipping on this machine with a message blaming "this platform/filesystem", now runs and passes -- it exercises the case it was written for instead of quietly not doing so.

Testing

Every site: an injected, version-independent monkeypatch test, paired with a "must not fire" control confirming genuine absence still reports as absent. Two sites also get a real chmod-based fixture that self-skips loudly with what went untested when the platform's permission model doesn't produce the effect (root, some filesystems, Windows' read-only attribute not blocking a listing) -- and, after the self-review fix, no longer skips on this machine's 3.14. doctor.py's exit 0 always, one VERDICT line contract verified unchanged on every new path.

Full suite: 2587 passed, 2 skipped (unrelated), 0 failed.

Closes #363.

fdaviddpt and others added 3 commits August 20, 2026 11:40
…losed one instance of (#363)

An unreadable parent directory made check_directory, check_jit_rules and
resolve_project_dir's own --root check swallow to a confident "does not
exist"/"no rules for this repo", with a remedy telling the reader to create
something that may already be there -- #341's own sentence one call site
over. Those three now go through a shared _dir_state(path) that returns a
third "unreadable" answer instead of folding it into absence, and
resolve_project_dir -- the entry point where the third-state contract is
established, not merely consumed -- gets the same treatment for its own
.git existence check.

merge_permission_state's path.exists() now routes an unreadable settings
candidate into the "unknown" bucket the function already had and never
reached. The two narrow glob filters (jit_hook_roots, _jit_layer_verdict)
now use a swallow-only _safe_is_dir so one unreadable candidate no longer
wipes every candidate already found in the same scan, mirroring the
swallow-vs-raise fix #359 made for _safe_is_file.

Every site is covered by an injected, version-independent test plus a real
chmod-based fixture where the platform's own permission model can produce
it, each with a "must not fire" control confirming a genuine absence still
reports as absent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9R2KfyZyHEXhMrE1VcNwH
…the exact interpreter this fix was for (#363)

A spawned audit (second pass, after the first pass's return went unstated
and was re-run) caught that `_dir_state` classified "unreadable" by
catching `OSError` out of `Path.is_dir()`. Measured directly on this
machine's local Python 3.14 install: `is_dir()` against the exact
unreadable-parent fixture this PR's own tests build returns `False` with no
exception at all, so the `except OSError` branch was unreachable there and
`check_directory`/`check_jit_rules`/`resolve_project_dir` would have
silently degraded back into the confident-absence bug #363 exists to fix --
on precisely the interpreter CLAUDE.md already names as the one that
swallows, and that the real chmod-based test in this same PR was quietly
self-skipping on for that reason, with a skip message that blamed "this
platform/filesystem" rather than the interpreter.

`_dir_state` now calls `path.stat()` instead, which does raise on 3.14
(also measured directly). `stat()` also raises `FileNotFoundError`/
`NotADirectoryError` for a genuinely absent path, which the first version
of this change folded into "unreadable" and broke -- caught immediately by
this file's own must-not-fire controls, which is the reason they exist.
Both exception types are caught ahead of the general `OSError` arm by type,
not by an errno table CLAUDE.md already warns Windows folds unevenly.

The three injected tests that target `_dir_state`'s consumers now patch
`Path.stat` instead of `Path.is_dir`, matching what the code actually
calls; the real chmod-based fixture now probes `stat()` too and no longer
self-skips on this machine -- it exercises the exact case it was written
for instead of quietly not doing so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9R2KfyZyHEXhMrE1VcNwH
…comments (#363)

Spawned reviewer finding: the section-header comments above each group of
tests cited pre-fix line numbers (the sweep issue's own approximate
pointers), and the fix shifted every call site downward by 40-80 lines
adding the new _dir_state/_safe_is_dir helpers. Not load-bearing -- the
tests call the right functions by name regardless -- but a stale line
number next to working code is exactly the kind of thing nobody re-checks.
Named by function instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9R2KfyZyHEXhMrE1VcNwH
@fdaviddpt
fdaviddpt merged commit be36015 into main Aug 20, 2026
14 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.

Six more is_dir()/exists() swallow sites in doctor.py, enumerated with the clean ones — the class #341 closed one instance of

1 participant