38b9f9e286
The EAGAIN retry shipped in v0.10.3 was too small to matter. It waited a flat 3 x 10ms, which covers a momentary dip and not the real thing: a CI runner building and testing in parallel stays process-starved for hundreds of milliseconds at a stretch. `subprocess_run_spawn_failure` kept failing WITH the retry in place — macos-15-intel on a release matrix, then macos-14 under ThreadSanitizer, twice on the same SHA, which is what moved this out of the flake bucket. A fixed short delay just samples the same congested instant repeatedly; doubling walks out of it. Now 10/20/40/80/160/320ms — about 0.6s of total patience. That is invisible next to spawning a process that does real work, and a machine still refusing after it is genuinely out of capacity, where failing fast beats hanging. The previous attempt shipped a constant with no way to prove it worked, so this adds the seam that was missing: CBM_ENABLE_TEST_SEAMS builds can force N simulated refusals, and two tests pin the behaviour deterministically — fewer refusals than the budget must still spawn, more must fail rather than retry forever. Both are revert-checked. The seam compiles out entirely in production (cppcheck correctly called the always-false branch dead code, so it is gone rather than suppressed). Two things the tests caught that review had not: - The macOS primary path is posix_spawn, not fork; injecting only into the fork fallback exercised nothing on macOS. Both paths now carry it. - cbm_fork_with_retry ended with an unconditional fork() OUTSIDE the loop, so the final attempt could never be reached by a test. Every attempt now goes through the same branch. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>