Ban repo_root()-derived sys.path inserts in tests (worktree runs silently exercise main's code)
Highlight text on any card (body, plan, or an event) to anchor a comment, or leave a whole-task comment here. Mention @claude to summon a reply.
No comments yet.
Goal
Add a workflow_lint.py check (plus its invariant test) that FAILS on any
sys.path.insert(...) / sys.path.append(...) under tests/ whose argument is
derived from task_workflow.repo_root(), and require the tree-local
Path(__file__).resolve().parents[1] / ... form instead.
Why (concrete incident, task #2164)
tests/test_issue1482_densesae_fullwidth.py carried, at module level:
from explore_persona_space.task_workflow import repo_root
sys.path.insert(0, str(repo_root() / "scripts"))
repo_root() branch-guards to the MAIN checkout. Under any worktree-run pytest
session that had two consequences, both silent:
- The test exercised main's copy of the module under test, not the tree's.
It imports
issue1482_densesae_fullwidthfromscripts/. From a worktree,repo_root()/scriptsis the MAIN checkout — so a branch modifying that driver had its own test import a different file than the one it changed. A real regression in that driver could pass its own test on the branch and only surface after merge. - It leaked a foreign checkout's
scripts/ontosys.pathfor the whole pytest session.tests/test_backend_poll.py's #1296 negative control scrubs only entries resolving to the LOCAL tree'sscripts/, so the leaked entry survived the scrub and a bareimport runpod_apistill resolved:Failed: DID NOT RAISE ModuleNotFoundError.
Effect (2) is how it was found: it turned #2164's Step 10d pre-push lint gate
red as a payload-attributed NEW node, because that gate runs its mapped-test
legs asymmetrically — gated from the worktree (where repo_root() does not
match the cwd tree), baseline from the repo root (where it does, so the entry is
scrubbed and the control passes). Diagnosis cost roughly ten probe commands plus
a full ~20-minute gate re-run on a branch whose payload was innocent.
#2164 FIXED the single offending instance (landed on main via 5c91482fce;
fix commit 69a58d6e5be439bc6465f767609267a75f784ad9) and swept tests/,
scripts/, and src/ — it was the only occurrence at that time. This task adds
the mechanical guard so it cannot come back, since nothing currently forbids it.
Scope
- A
workflow_lint.pycheck flaggingsys.path.insert/appendundertests/whose argument expression mentionsrepo_root(.workflow_lint.pyalready carries an adjacent repo-rootsys.pathguard (see its docstring around the_ensure_repo_root_on_syspath()exemplar) — extend that family rather than adding a parallel mechanism, and bundle it into the no-flags default run so the Step 9c / Step 10d gates pick it up. - An invariant test for the new check, including a NEGATIVE control proving it fires on the pre-fix form (the deliberate-breakage non-vacuity pattern used by #2164's own anti-drift test).
- Prefer
monkeypatch.syspath_prependover a bare module-level insert in the guidance text:tests/test_clean_experiment_downloads_symlinks.py:57already documents that choice ("so the entry is restored"), so the repo has a sanctioned restoring form to point at. - Secondary, only if cheap: a one-line diagnostic note in the SKILL.md gate subsection saying that a NEW node appearing ONLY in the gated leg can be a worktree-vs-root environment artifact rather than a payload regression, with the two-file reproduction shape as the check. This is the diagnosis-cost half of the incident; the lint check above is the prevention half.
Out of scope
- Changing the gate's asymmetric baseline placement (gated
$WT/ baseline repo root). That asymmetry is deliberate per SKILL.md § Baseline semantics (the baseline must be a payload-free tree) and is not the defect here. - The unrelated pre-existing
test_setsid_child_survives_group_killmain-red, parked separately from #2164 in the #1681 urgent form.
Provenance
Surfaced by task #2164's Step 10d pre-push workflow-lint gate on 2026-08-07:
verdict block on a NEW failing node that proved not to be payload-attributable
(both files involved were byte-identical to origin/main on that branch).