cmux-next: a quit in the Swift test helper no longer ends the whole test run

Merged
TE
15d ago
+166-6

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.requestQuit
  • UpdateDropBack.relaunchAfterExit
  • DebugUpdater.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 in refusedTerminations). The three sites use it.
  • CmuxNextTestExitTrace is a test-only C target linked into CmuxNextAppTests. Every exit() 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 Autofix with [code]smith
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.