test: pay macOS's first-run check before timing wrapper fixtures

Merged
AU
17d ago
+126-38

The Claude wrapper regressions lane failed in 12 of 45 runs on the fleet Mac minis and 0 of 138 on Blacksmith, each time with a different message. The mutual-shim test in the CLI product lane timed out the same way. In both, the test's own fixtures lost a race with a macOS first-run check. Each fixture now pays that check before its timed run. No wrapper budget or test guard changes.

Cause

macOS 26 blocks the first run of every newly written executable, copies included, while syspolicyd assesses it: a Gatekeeper scan, a notarization lookup and an XProtect scan. That costs 0.15–0.4 s on an idle Mac and seconds on a loaded mini. A second run of the same file costs 5–20 ms.

tests/test_claude_wrapper_hooks.py writes a fresh fake claude and fake cmux for every case, and copies the wrapper to a fresh path, then runs them for the first time inside the wrapper's own budgets:

  • The 1 s hook-settings generator. When the check eats that budget, the wrapper falls back to minimal hooks. That produces:
    • standard settings: Claude must receive the bundled CLI document byte for byte
    • cmux hooks lost
    • expected one Node validation, ran 0
  • The 0.75 s claude --help probe. When the check eats that budget, the wrapper caches an empty catalog. That produces:
    • fallback success: failure was not cached: []
    • expected one help call: []
    • a ProcessLookupError that hid the rest of the results
  • The 2 s and 5 s whole-process timeouts of the large-settings checks, which time the wrapper copy's first run.

tests/test_claude_wrapper_mutual_shim_loop.py runs fresh wrapper copies and shims inside each 5 s guard, so it hit the same check.

Real installs were run before, so they never pay this inside those budgets. #15768 fixed the Hermes wrapper tests the same way.

Change

  • Every fixture now exits at once when CMUX_TEST_PRIME_EXEC is set, and is run once that way as soon as it is written.
  • Every wrapper copy, in both files, is primed with PATH=/usr/bin:/bin and no other environment. The wrapper then finds no claude and exits 127 before it writes anything. It has no priming guard of its own, and the runner's PATH could reach a real claude.
  • The native fake claude is primed with --help.
  • The cancellation case records a failure instead of raising when the wrapper exits before the interrupt. It also waits until the help PID log holds both PIDs: the shell creates that file before printf writes to it.

Verification

  • Under load, locally: 6 parallel workers ran the timing-sensitive checks. Unprimed, 5 of 6 workers failed with the CI messages; primed, 0 of 6.
  • Claude wrapper, locally: python3 tests/test_claude_wrapper_hooks.py passes in full with the CMUX* variables of the cmux session it ran in removed (f21ecc11f27). With them set, only the computer use socket-path checks fail, because they read this machine's own cmux runtime ID; CI has no such variables.
  • Claude wrapper, CI: this PR's Claude wrapper regressions lane runs on the fleet minis. It passed on 7bfa3b3. On c9b43ec its first attempt ran the file in 153 s with 0 failures, but the job had already waited 13 minutes for a fleet slot, so the lane's 15-minute limit cancelled it. The rerun passed.
  • Mutual shim: this PR skips the macos job, which hosts the only lane that runs test_claude_wrapper_mutual_shim_loop.py. The validation PR #15960 includes this change, and its CLI product tests lane ran the file on cmux14: tests/test_claude_wrapper_mutual_shim_loop.py ok in 10.8s (job). It also passes locally.

Refs #15488.

Changelog

  • none

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Tests
    • Improved setup for timed wrapper and hook tests by priming test executables and copied wrappers before use.
    • Added safeguards to prevent fixture executables from performing normal work during priming, and standardized the restricted environment used for setup.
    • Updated cancellation-test handling for cases where the help process does not start or the wrapper exits before process-group signaling.