fix(tests): isolate xdist polluters blocking -n auto default (W3) #46
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/xdist-polluter-isolation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
W3 — hunt the
-n autopolluters blocking parallel-by-defaultFollow-up to the "xdist by default" item in
docs/testing-lanes.md(after #782 engine-state isolation and #786 slow-lane split). Goal: get the fast lane (pytest -m "not quarantine and not slow") reliably green under-n autoso it can become the default, without weakening any assertion.Method
-n autorepeatedly (full-suite + smaller targeted repeats forcing worker overlap on suspect files) and diffed failure sets across runs — a test that fails under-n autobut passes serially/in isolation is a polluter signature.docs/testing-lanes.md's own "Follow-up: xdist by default" note, which already named the suspect categories (engine_state/writers,evals/.../report.jsonwriters,teaching/proposals/writers) and one specific documented flake (test_workbench_replay::test_replay_leaves_no_trace).-n autorun failed withAssertionError: Directory .../engine_state was mutated!and passed cleanly in serial isolation — the textbook signature.Polluter table
test_workbench_replay.py::test_replay_leaves_no_traceengine_state/dirPath("engine_state")instead of readingengine_state._DEFAULT_DIR(which the root-conftest autouse fixture already redirects per-test). A victim, not a writer: any concurrent worker's write to the real shared dir made the before/after snapshot diff. Matches the flake already documented indocs/testing-lanes.md.engine_state._DEFAULT_DIRdynamically instead of a hardcoded constant.test_rat1_end_to_end_admission.py(test_wrong_zero_preserved_on_train_sample,test_case_0050_remains_refused_after_rat1) +test_wave_a_multiplicative_aggregation_injector.py(test_wrong_zero_preserved,test_case_0050_remains_refused) — 4 tests, 2 filesevals/gsm8k_math/train_sample/v1/report.json(committed repo file)runner.py'smain()hardcodes the write path with no override. All 4 tests spawn it as a subprocess and read the same file back — a write race under-n auto, and it was overwriting a git-tracked fixture on every serial run too. Confirmed downstream victim:test_gsm8k_sealed_attempt_scout.py::test_report_json_mtime_unchanged_by_scout_importasserts the file's mtime is stable across an unrelated import.CORE_GSM8K_TRAIN_SAMPLE_REPORT_PATHenv override inrunner.py(default unchanged — real operator usage untouched). Pointed the 4 test call sites attmp_path.tests/test_cli_demo.py—TestDemoSubcommand+TestDemoPreambles, ~11 testscore.cli._DEMO_RESULTS_DIR(evals/forward_semantic_control/results/)core demosubcommand invocation writes report files into this dir and rebuildsindex.jsonvia a glob-scan-then-rewrite. ~11 tests in this file callcli.main(["demo", ...]), racing on the same directory.cli._DEMO_RESULTS_DIRto a per-test tmp dir (same idiom as #782's engine-state fixture). This also unmasked a real latent bug:test_demo_list_results_indexes_reportsandtest_demo_list_results_json_well_formednever wrote their own report — they silently depended on a sibling test's leftover file in the shared dir, which only worked by accident of pytest's file-declaration run order. Made both self-contained.tests/test_teaching_queue.py::test_read_only_invariantengine_state/dirproject_root / "engine_state"in a 4-directory read-only-invariant snapshot, bypassing the per-test isolation. Caught live: a full-n autorun failed withAssertionError: Directory .../engine_state was mutated!, passed cleanly in serial isolation.engine_state._DEFAULT_DIRdynamically for theengine_stateentry. The other 3 snapshotted dirs (teaching/proposals,packs,contemplation/runs) have no equivalent per-test isolation idiom yet and are untouched — this test's own commands don't write to them, so they weren't implicated.No assertion weakening, no test deletion, no global autouse fixture masking real bugs. All fixes preserve the real (non-test) default behavior byte-for-byte when unpatched/env var unset — verified by inspecting the resolved default path with the override unset.
Honesty note on live reproduction
Clusters 1 and 4 were caught live in a real
-n autorun (the exact failure text is quoted above). Clusters 2 and 3 are confirmed by source (shared repo-relative write path, multiple concurrently-schedulable writers/readers, no isolation) plus cluster 2's confirmed downstream victim test, but I could not force a live crash for them in an 8-iteration-n 8targeted repeat loop — the write itself is a single fast syscall so the corruption window is narrow, and this machine had heavy contention from concurrent sessions during part of this session (a separate worktree's own full-suite-n autorun competing for the same cores). The fix is the same isolation idiom regardless of whether I could force the crash window open.Verification
evals/gsm8k_math/train_sample/v1/runner.pydefault (unset env var) still resolves to the real committed path — real operator usage unaffected.-n 8loop over the 7 affected files, before and after the fix — did not force-reproduce the underlying race live at this small scale (see honesty note above); confirms no regression either way (72/72 passed post-fix across 5 repeats).-n autoruns, zero polluter-caused failures (rebased onto latestforgejo/mainpartway through, which also picked up an unrelated concurrent PR (#45) that cleared 5 pre-existing physics/rotor reds — final run is fully green modulo one brand-new, unrelated, freshly-introduced doc-consistency red from a governance PR (#43) that landed during this verification window, confirmed to fail in serial isolation too — not a polluter, not touched by this PR):11667 passed, 106 skippedin 15:0711667 passed, 106 skippedin 12:2111667 passed, 106 skippedin 15:24forgejo/mainafter #45 merged):11671 passed, 106 skipped, 1 unrelated pre-existing red, in 14:08uv run core test --suite smoke -q: 175 passed (2:36).Wall-clock
Docs baseline (
docs/testing-lanes.md, quiet 10-core macOS): fast lane serial ~26 min →-n auto~9.5 min, but not wired in as default because of exactly these hermeticity gaps.This machine had fluctuating load from concurrent sessions throughout this session (other worktrees' own test runs sharing the same cores), so absolute numbers aren't a clean apples-to-apples baseline. Same-session data point: one fully unpatched
-n autorun under heavy contention (a sibling session's own full-suite-n autorun active concurrently) took 29:14; three post-fix runs under lighter (but not zero) contention averaged ~14:17. The reliability delta is the real result here — before this PR,-n autohad known, reproducible-if-you're-unlucky failure modes (one of which I hit live); after, 3/3 consecutive full runs and a 5-repeat targeted loop show zero polluter-caused failures.[Verification]:
uv run pytest -m "not quarantine and not slow" -n auto -q— 3 consecutive clean runs (11667 passed / 106 skipped each, zero polluter-caused failures).uv run core test --suite smoke -q— 175 passed.Hunt for the -n auto fast-lane polluters flagged in docs/testing-lanes.md ("Follow-up: xdist by default"). Three root causes, all shared-repo-path writers with no per-test isolation, following the #782 monkeypatch idiom: 1. tests/test_workbench_replay.py::test_replay_leaves_no_trace hardcoded `Path("engine_state")` (the real shared dir) instead of reading `engine_state._DEFAULT_DIR`, which the root-conftest autouse fixture already redirects per-test. It was a victim, not a polluter: any concurrent worker writing the real dir made this snapshot-diff flake. Fixed by reading `engine_state._DEFAULT_DIR` dynamically. 2. evals/gsm8k_math/train_sample/v1/runner.py hardcoded its report.json output to the committed repo path with no override. Two test files (test_rat1_end_to_end_admission.py, test_wave_a_multiplicative_ aggregation_injector.py; 4 tests total) spawn it as a subprocess and read the same file back — a write race under -n auto, and a confirmed downstream victim (test_gsm8k_sealed_attempt_scout.py:: test_report_json_mtime_unchanged_by_scout_import asserts the file's mtime is stable). Added an optional CORE_GSM8K_TRAIN_SAMPLE_REPORT_PATH env override (default unchanged) and pointed the 4 call sites at tmp_path. 3. core/cli.py's `_DEMO_RESULTS_DIR` (evals/forward_semantic_control/ results/) is written, glob-scanned, and index.json-rebuilt by ~11 tests across tests/test_cli_demo.py's TestDemoSubcommand and TestDemoPreambles classes. Added an autouse fixture monkeypatching `cli._DEMO_RESULTS_DIR` to a per-test tmp dir. This also unmasked a latent order-dependent coupling: test_demo_list_results_indexes_reports and test_demo_list_results_json_well_formed never wrote their own report, relying on a sibling test's leftover file in the shared dir (silently correct only because pytest ran the file in declaration order). Made both self-contained. No assertion weakening, no test deletion, no global autouse fixture masking real bugs. All three fixes preserve the real (non-test) default behavior byte-for-byte when unpatched/env-unset. Verification: 8x targeted -n 8 loop over the 7 affected files (before and after) did not force-reproduce the underlying race live (narrow timing window, small-scale run) — confirmation is source-level (hardcoded shared paths bypassing the established isolation idiom) plus the prior documented flake for test_replay_leaves_no_trace in docs/testing-lanes.md. Full fast-lane -n auto run recorded in the PR description.