Skip to content

test: isolate each test's working directory and guard the temp-root chain (see #235, #261) - #266

Merged
ViperJuice merged 2 commits into
mainfrom
impl/235-slice-a-cwd-isolation
Sep 20, 2026
Merged

ViperJuice merged 2 commits into
mainfrom
impl/235-slice-a-cwd-isolation

Conversation

@ViperJuice

@ViperJuice ViperJuice commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator

Implements slice A of the hermeticity plan (.consiliency/plans/detailed-test-cwd-filesystem-isolation-20260920-0814.md, merged in #263). Tests only — no src/ changes.

The headline

On a /mnt/<volume>/worktrees/... checkout whose volume root holds node_modules, the suite went from 107 failed / 106 errors → 4011 passed, 0 failed. That artifact has been a standing caveat on every verification run; it is now fixed rather than worked around.

What it does

  • isolate_cwd (autouse) runs each test from a private directory under pytest's temp root — a sibling of tmp_path via mktemp, never a child (a child would make files a test writes into tmp_path ancestors of its own cwd). It requests isolate_trust_store explicitly, because pytest does not guarantee autouse ordering and the fake HOME must exist first.
  • assert_clean_ancestor_chain (session) fails once, naming the path, when the temp root's own ancestors carry a package.json/node_modules — the one case isolate_cwd cannot escape, since tmp_path lives under gettempdir(). It walks only that chain, never Path.cwd(): a session fixture observes the invocation directory, so walking cwd would fail exactly the hosts isolate_cwd has already fixed.
  • real_cwd marker registered, scoped narrowly to tests whose subject is the invocation directory.

Why #261 was really failing

_find_project_manifest walks up from Path.cwd() and stops at Path.home() (the #243 guard). isolate_trust_store redirects HOME to a fake dir, so that stop never matched the real home — from a checkout under $HOME the walk sailed past it, found the developer's own ~/.pmcp/manifest.yaml, and gated it as a project overlay. The isolation fixture caused the failure. chdir-ing under the temp root makes the walk's existing temp_root stop fire.

Verification (all run, not asserted)

Check Result
#261 on main from a real-home cwd fails (the "before")
#261 with the fix passes
Mutation: remove the chdir red (assert 2 == 1) — falsifiable
/tmp/package.json planted one named guard error, not a 107-failure cascade
node_modules on a repo ancestor (not a tempdir ancestor) 100 npm tests pass, guard silent — isolated, not detected
cwd-subject test without the marker passes
Full suite on the polluted /mnt host 4011 passed, 0 failed
ruff check / ruff format clean

Scope note the plan did not anticipate

Implementation surfaced a class plan review could not: ten tests read repo data files through cwd-relative paths (Path("src/pmcp/manifest/manifest.yaml") etc.), which break once cwd moves. They are not cwd-subjects, so real_cwd would misuse the marker against the plan's own definition — they now anchor to a repo root via Path(__file__), the idiom tests/test_workflow_guards.py already used. Only test_the_allowlist_matches_the_committed_tree takes the marker: its subject genuinely is the committed tree at the invocation root, and scripts/check_workflows.py resolves .github/workflows relative to cwd by design.

npm_resolver._has_local_prefix is deliberately unchanged — it replicates npm's real local-prefix rule, and weakening it to make tests pass would be the actual bug.

Closes nothing on merge by design; #261 is closed by this behaviour but left to a human to confirm. See #235, #261.

🤖 Generated with Claude Code

https://claude.ai/code/session_01H4VQypRNMRJ4caC64P7M3w


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

ViperJuice and others added 2 commits September 20, 2026 22:48
…hain (see #235, #261)

Implements slice A of the hermeticity plan.

isolate_cwd (autouse) runs every test from a private directory under pytest's
temp root, a SIBLING of tmp_path via mktemp -- never a child, which would make
files a test writes into tmp_path ancestors of its own cwd. It requests
isolate_trust_store explicitly, since pytest does not guarantee autouse order
and the fake HOME must exist first. Two leaks close:

* #261: manifest.loader._find_project_manifest walks up from Path.cwd() and
  stops at Path.home(); because isolate_trust_store redirects HOME to a fake
  dir, that stop never matched the REAL home, so from a checkout under $HOME the
  walk reached the developer's own ~/.pmcp/manifest.yaml and gated it as a
  *project* overlay -- a second consent refusal. The isolation fixture caused the
  failure; chdir-ing under the temp root makes the walk's existing temp_root stop
  fire. Verified: fails on main from a real-home cwd, passes here, and removing
  the chdir turns it red again (assert 2 == 1).
* The npm local-prefix artifact: a checkout under a directory holding
  node_modules put that directory on every un-chdir'ed test's ancestor chain and
  inverted npm-identity assertions. On this /mnt worktree (volume-root
  node_modules present) the suite went from 107 failed/106 errors to 4011 passed.

assert_clean_ancestor_chain (session) fails ONCE, naming the path, when the temp
root's own ancestors carry a package.json/node_modules -- the one case
isolate_cwd cannot escape, since tmp_path lives under gettempdir(). It walks
only that chain, never Path.cwd(): a session fixture sees the invocation dir, so
walking cwd would fail exactly the hosts isolate_cwd has already fixed. Verified
both directions: planting /tmp/package.json raises the single named error;
planting node_modules on a repo ancestor leaves 100 npm tests passing and the
guard silent.

npm_resolver._has_local_prefix is deliberately unchanged -- it replicates npm's
real local-prefix rule, and weakening it to make tests pass would be the bug.

Implementation surfaced a class the plan did not anticipate: ten tests read repo
data files through cwd-relative paths. They are not cwd-subjects, so marking
them real_cwd would misuse the marker; they now anchor to a repo root via
Path(__file__), the idiom already used in tests/test_workflow_guards.py. Only
test_the_allowlist_matches_the_committed_tree takes real_cwd -- its subject IS
the committed tree at the invocation root, and scripts/check_workflows.py
resolves .github/workflows relative to cwd by design.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4VQypRNMRJ4caC64P7M3w
…is polluted (see #235)

Panel CR (codex, blocking): assert_clean_ancestor_chain raised RuntimeError from
a SESSION-scoped fixture. Pytest caches that setup failure and re-reports it
against every dependent test, so the "one clear error" the guard exists to give
became the very cascade it replaces. Measured on this branch: one planted
/tmp/package.json produced 100 errors, not one -- my acceptance criterion said
"ONE named guard error" and was simply false.

My own verification missed it because I grepped the output for the expected
message with head -6 instead of counting occurrences; the cascade was visible in
that output all along.

pytest.exit(msg, returncode=1) ends the session at first detection. Re-measured
by counting: 0 errors, 1 guard message, "no tests ran".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4VQypRNMRJ4caC64P7M3w
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.

1 participant