cmux-next: a quit in the Swift test helper no longer ends the whole test run
The CmuxNext Swift test run (cmux-next swift test) has ended with status 0 partway through since at least 2026-10-20 14:35Z. Hundreds of tests are left "started and never finished" (742 in #23599's job 119381263992), with no crash report and no signal. WindowServer logs an ordinary process death. HomeOpenFocusesComposerTests is usually the last suite to report, because its click helper spins the main run loop. About 400 PR swift-test jobs ran since 10-19 12:00Z, and one succeeded. Base pushes skip this job by design (cmux-next.yml:2360), so base never showed it.
Likely cause. Three quits were scheduled on the main run loop as a bare NSApp.terminate(nil):
QuitCoordinator.requestQuitUpdateDropBack.relaunchAfterExitDebugUpdater.terminate
In a process whose app never ran (the Swift test helper), AppKit's terminate is exit(0). It fires as soon as any test spins the main run loop.
Fix
NSApplication.terminateFromRunLoop(CmuxNextCompat) schedules the quit exactly as before when the app is running. When the app never ran, it refuses, and prints the call site and the stack that asked on stderr (also kept inrefusedTerminations). The three sites use it.CmuxNextTestExitTraceis a test-only C target linked into CmuxNextAppTests. Everyexit()of the test process prints its stack, so if anything else ends the run, the CI log names the caller.tests/test_cmux_next_deferred_terminate.py(linux-guard, run in cmux-next guards) keeps bare deferred terminates out.
Tests: TestProcessExitTests. A quit asked in the test process is refused and names its call site; before this change, that test ended the whole run. A second test checks that the exit tracer is installed.
The caller that triggers it in the suite will be named here once this PR's swift-test log shows the refusal line.
No capture: no UI change (quit plumbing and test support).
Agent session: cosmic-gray
๐ค Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Fixes the cmux-next swift test run ending with status 0 partway through since at least 2026-10-20, leaving hundreds of tests started but never finished with no crash report. A bare NSApp.terminate(nil) scheduled on the main run loop became exit(0) in the test helper process, whose app never ran, firing as soon as any test spun the run loop.
Adds NSApplication.terminateFromRunLoop (CmuxNextCompat): it schedules the quit exactly as before when the app is running, but refuses when it is not, printing the call site and the asking stack to stderr and recording it in refusedTerminations. The three quit sites โ QuitCoordinator.requestQuit, UpdateDropBack.relaunchAfterExit, DebugUpdater.terminate โ now use it. Test-only CmuxNextTestExitTrace prints the stack of every exit() of the test process so any other exit caller is named in CI logs; it installs only when tests actually run, staying quiet during --list-tests/--dump-tests-json. A new linux-guard (test_cmux_next_deferred_terminate.py) keeps bare deferred terminates out. TestProcessExitTests verifies a quit in the test process is refused with its call site named, and that the exit tracer is installed.
No UI change; quit plumbing and test support only.