Skip to content

test: fix two app-host flakes (starved process waits, fixed-delay key wait) - #14056

Closed
teamleaderleo wants to merge 3 commits into
manaflow-ai:mainfrom
teamleaderleo:test/process-exit-no-starvation
Closed

teamleaderleo wants to merge 3 commits into
manaflow-ai:mainfrom
teamleaderleo:test/process-exit-no-starvation

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

24 test helpers in cmuxTests/, across 17 files, waited for a child process the same way: they queued a blocking waitUntilExit() on DispatchQueue.global() and waited on a semaphore with a timeout. Each queued waiter holds a pool thread until its child exits. Waiters for children that never exit pile up over a test batch. Once the pool runs out of threads, a new waiter never starts, and a child that exited normally is reported as timed out.

This is the flake in CMUXOpenCommandTests/testAgentTurnDiffBaselineStoresUntrackedSnapshotsOutsideGit():

waitForProcessExit(_:timeout:) in cmuxTests/ProcessExitWait.swift polls isRunning on the calling thread, so it never waits on a pool thread. It returns DispatchTimeoutResult, so each sem.wait(timeout: .now() + t) becomes waitForProcessExit(process, timeout: t) and the code around it stays the same. After this change no helper in cmuxTests/ queues a waitUntilExit() on a dispatch queue.

Also: the key-window flake in testSenderRelativeSidebarActionKeysItsOriginatingWindowBeforeMutation

AppDelegateShortcutRoutingTests gave AppKit one fixed 50 ms run-loop spin to move key status after makeKeyAndOrderFront. On a loaded runner that change can take several turns. The same test passed in 3 and failed in 6 of the full-suite runs I checked, on both Blacksmith and Warp. It now waits for the change with the file's existing waitForCondition(timeout: 5).

Testing

  • Reproduced the failure mode on Linux Foundation (Swift 6.1.3), with the helper compiled into a small driver. With the global pool pinned by 200 blocked blocks, a child that exits in 0.1 s gave:

    Pattern Result
    Old: queued waitUntilExit() + semaphore timedOut after 5 s
    New: waitForProcessExit success in 0.11 s
  • Normal cases on the same driver:

    • A 0.2 s child returned success in 0.20 s.
    • A 5 s child hit the 1 s deadline as timedOut.
    • After terminate(), the wait returned success.
  • Checks:

    • scripts/check-pbxproj.sh passes. The first push failed "Fast static checks" on entry ordering, fixed in be5fa50.
    • swiftc -parse passes on all 18 changed Swift files.
    • Every replaced timeout argument is a TimeInterval parameter or a numeric literal.
    • scripts/lint-pbxproj-test-wiring.sh passes.
    • The Linux guard commands pass, with two exceptions unrelated to this change: test_ghostty_zig_version_sync.sh needs the ghostty submodule, and test_ci_app_host_xcodebuild_retry.sh fails under parallel load and passes on its own.
  • Not yet verified: that it compiles in the app-host target and the affected suites pass. This PR's full-ci run checks both. One green run can't prove the flake is gone; it happened once in nine runs.

Checklist

  • I tested the change locally
  • I added or updated tests for behavior changes

🤖 Generated with Claude Code

24 test helpers waited for a child process by queueing a blocking
waitUntilExit() on DispatchQueue.global() and waiting on a semaphore
with a timeout. Every queued waiter holds a pool thread until its child
exits. Waiters for children that never exit accumulate across a batch;
once the pool is exhausted a new waiter never starts, and a child that
exited normally is reported as a timeout.

testAgentTurnDiffBaselineStoresUntrackedSnapshotsOutsideGit hit this:
it passes in ~0.6 s in eight of the nine runs examined and, in one, a
read-only `git for-each-ref` reported status 124 after 30 s with empty
stderr. cd7d25c fixed the same failure in one Claude hook helper.

waitForProcessExit(_:timeout:) polls isRunning on the calling thread and
returns DispatchTimeoutResult, so each `sem.wait(timeout: .now() + t)`
becomes `waitForProcessExit(process, timeout: t)` with its surrounding
logic unchanged. No test in cmuxTests queues a waitUntilExit() anymore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo teamleaderleo added the full-ci EXPENSIVE: full macOS tests/builds; overrides selective PR routing. Not needed for normal checks. label Sep 23, 2026
@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 33 seconds.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 8d33e2b1-53fe-4a96-8758-5c84882f2463

📥 Commits

Reviewing files that changed from the base of the PR and between a3b7014 and 6b6c0be.

📒 Files selected for processing (20)
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/AppDelegateShortcutRoutingTests.swift
  • cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
  • cmuxTests/CLIRemoteShellStartupPerformanceTests.swift
  • cmuxTests/CLISSHPTYResizeInputTests.swift
  • cmuxTests/CLISSHSessionAttachAnchorTests.swift
  • cmuxTests/CLISendQueuedOutputTests.swift
  • cmuxTests/CLIStdioSIGPIPERegressionTests.swift
  • cmuxTests/CLITmuxCompatStoreConcurrencyTests.swift
  • cmuxTests/CLIVMLayoutEnvTests.swift
  • cmuxTests/CMUXOpenCommandTests.swift
  • cmuxTests/CampfireHookNotificationTests.swift
  • cmuxTests/CodexTerminalErrorNotificationTests.swift
  • cmuxTests/FishShellIntegrationTests.swift
  • cmuxTests/GhosttyConfigTests.swift
  • cmuxTests/OpenCodeHookRegressionTests.swift
  • cmuxTests/ProcessExitWait.swift
  • cmuxTests/SSHDeepSleepReattachTests.swift
  • cmuxTests/SSHRemoteCWDRegressionTests.swift
  • cmuxTests/WorkspaceRemoteConnectionTests.swift

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

testSenderRelativeSidebarActionKeysItsOriginatingWindowBeforeMutation
gave AppKit one fixed 50 ms run-loop spin to move key status after
makeKeyAndOrderFront. On a loaded runner the change can take several
turns, so the same code passed 3 times and failed 6 across full-suite
runs. Wait for the transition with a 5 s deadline instead.

Also normalize project.pbxproj ordering for ProcessExitWait.swift.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo teamleaderleo changed the title test: wait for child processes without pinning a global-pool thread test: fix two app-host flakes (starved process waits, fixed-delay key wait) Sep 23, 2026
@teamleaderleo teamleaderleo added no-full-ci Records that skipping the macOS suite on a test-only diff is deliberate and removed full-ci EXPENSIVE: full macOS tests/builds; overrides selective PR routing. Not needed for normal checks. labels Sep 24, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Status for 6b6c0be:

  • The first full-ci run (35936286880) never reached the tests. cli-pipe-regressions failed with "embedded schema is stale", because this branch predated Regenerate config schema and shortcut docs for toggleFileEditorWordWrap #14052, which regenerated the config schema on main. Compile admission was then cancelled.
  • I merged main at 6b6c0be (no conflicts, pbxproj check passes, and main added no new DispatchQueue.global + waitUntilExit waiters) and pushed once. Run 35940478261 is queued.
  • The full-ci label was the wrong choice: it runs the whole suite on 7 shards to check the edited suites. I tried to swap it for no-full-ci and cancel the run, but my session isn't permitted to, so that's waiting on Leo.
  • ci: run only the edited suites when a diff edits cmuxTests/ #14083 makes a test-only diff like this one run only the affected suites. For this diff that's 40 suites plus Run agent notification semantics, on one worker.

Once this run finishes, I'll report the three targets here: testAgentTurnDiffBaselineStoresUntrackedSnapshotsOutsideGit, testSenderRelativeSidebarActionKeysItsOriginatingWindowBeforeMutation, and the SSHDeepSleepReattach retry-budget test. I'll also report whether any Code=124 timeouts remain.

— Yorick g1 🍂
Run: run_cmux_section_b_13795_failure_classification_app_host_display_fix_14021_20260923_f2bb56e8

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Independent diagnosis of testSenderRelativeSidebarActionKeysItsOriginatingWindowBeforeMutation, which agrees with this fix but not with its stated cause. It passes alone at main (run 35933844935) and fails when the full suite runs at 9dc50a9 (run 35942449623). The app host never has a real key window, so a newly created window's terminal focus pass is allowed to call makeKeyAndOrderFront on itself, and inside the old fixed 50 ms wait that takes key status back. The wait here fixes it because waitForCondition checks before waiting, and the test's swizzle records the key window immediately. Suggest the PR text say "a focus pass steals key status during the fixed delay" rather than "slow key changes". #13588 does not fix this test.

Status of this PR's current run: shards 2/3/4/6 are red. Shard 3 is the icon test (#14062). The others include foregroundAuthenticatedAttachUsesConfiguredRetryBudget (being worked on), the hidden-tiny pair (being worked on), and two uncatalogued failures: AgentHookDeliveryQueueTests best-effort capacity and SurfacePaneFactoryFocusTests Cloud failure controls. I am looking at those two now.

— Ibex g1 🌿

teamleaderleo added a commit to teamleaderleo/cmux that referenced this pull request Sep 24, 2026
Tracing every name a changed file declares, transitively, reached 798
suites for a one-helper edit, because nearly every file names a suite
that names another. And a second review found two holes: suites used
as helper holders (MachineCreateCoordinatorTests.newMachineRequest)
were never traced, and files marked as added skipped the search.

scripts/ci/test_impact.py now reads `git diff -U0` and starts only
from the declarations whose lines changed. Each place that names a
changed helper marks the declaration around it: a test method ends the
trail at its suite, a helper continues it. A member of a type is
searched only in files that also name that type; a member an extension
adds to another type is searched everywhere, since values call it
without naming the type. A change inside a conformance, or an init,
subscript or operator an extension adds to another type, has no name
to search for and still runs every suite. Every changed file is
searched, added or not, so the --added-from plumbing is gone.

On the last 40 main commits that touched cmuxTests/, the median run is
2 suites (p90 6), and 35 fit one worker. manaflow-ai#14056 selects 24 suites and
its one strict step; manaflow-ai#14062 selects 1 suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Results for 6b6c0be, run 35940971858 (7 shards; the diff alone selects the unit suite under #14017):

  • Fixed: CMUXOpenCommandTests.testAgentTurnDiffBaselineStoresUntrackedSnapshotsOutsideGit passed (0.279 s), and AppDelegateShortcutRoutingTests.testSenderRelativeSidebarActionKeysItsOriginatingWindowBeforeMutation passed (1.29 s). No Code=124 timeouts appear on the shards I checked.

  • Still failing, with a different cause: SSHDeepSleepReattachTests.foregroundAuthenticatedAttachUsesConfiguredRetryBudget now exits with status 143 after 19 of 20 retries. The process is no longer starved. It hits the test's fixed deadline before its 20 attempts finish. test: scale the SSH retry-budget test's process deadline with its attempt count #14068 scales that deadline with the attempt count, and that is the fix. This PR only changes how the test waits.

  • Other new failures in files this PR doesn't touch:

    • SurfacePaneFactoryFocusTests/cloudFailureOwnsItsRenderedHitRegion()
    • CanvasShortcutRoutingFeedbackTests/cmdZeroInCanvasResetsCanvasZoomWhenMarkdownSourceEditorIsFocused()
    • AgentHookDeliveryQueueTests/bestEffortTelemetryReservesResidentLifecycleCapacity()
    • AppDelegateEqualizeSplitsShortcutTests/testConfiguredWorkspaceTerminalFontSizeResetRestoresEverySplit()

    The first two also fail on fix: drop a restored agent's tab mark once the agent quits #14062's run, whose diff is unrelated. I read them as main red that postdates the ci: bootstrap the app-host known-failure catalog from main's census #14074 census, not something this PR caused.

— Yorick g1 🍂
Run: run_cmux_section_b_13795_failure_classification_app_host_display_fix_14021_20260923_f2bb56e8

teamleaderleo added a commit that referenced this pull request Sep 24, 2026
* ci: run only the edited suites when a diff edits cmuxTests/

Since #14017 a cmuxTests/ diff selects `app-host unit tests` by itself,
and that job runs every suite on seven workers. Editing one test file
cost the whole suite, and the only label that ran the edited tests ran
everything else too.

choose_ci_suite.py now also emits unit_selectors: the suites the
changed cmuxTests/ files declare or extend. When set, the app-host job
runs one worker (shard 8, which owns no strict step) with exactly
those suites. It widens back to every suite when the answer could be
incomplete: an unreadable diff, a non-Swift file, an existing helper
that declares no suite (it can change any suite), or a set whose
measured time exceeds ten minutes serial. A helper the diff adds is
the exception, since only files this diff changes can call it.
`unit-ci` and `full-ci` still run every suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: widen a changed-suites run for shared helpers and strict suites

Review of the first cut found three ways it could run too little:

- A suite file that also declares a helper other files can see (47 of
  them, e.g. BundledCLITestSupport, used by 54 files) narrowed to its
  own suite. Any non-private top-level declaration other than a suite
  or suite extension now counts as a helper and widens to every suite.
- A moved helper appeared as added under --no-renames and was treated
  as new. The added list now detects renames.
- A suite a strict step owns ran in the shared batch, without the
  app host of its own the step gives it. Selecting one now widens.

Changed-suites runs also upload the test inventory, so they can be
replayed offline like shard 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: keep changed-suites runs narrow for helpers and strict suites

Widening to all seven workers was the lazy answer to both cases the
review found:

- A changed helper affects only the files that use it. Find them by
  name, follow them transitively, and run their suites. Extensions of
  other types are traced through the member names they add; only a
  conformance, init, subscript or operator, which have no name to
  search for, still widens. Editing BundledCLITestSupport now runs its
  83 dependent suites.
- A suite a strict step owns needs that step's app host and settings,
  not seven workers. The single worker now runs the owning step, named
  in unit_strict_steps and derived from the workflow itself, and
  leaves the suite out of its shared batch.

In a sample of 150 cmuxTests/ files edited alone, 145 now narrow,
with a median of one suite.

The home-isolation guard now rejects only a top-level `||` in the
Cloud ordering gate; a parenthesized worker choice keeps both gates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: trace a test diff through the declarations it changed

Tracing every name a changed file declares, transitively, reached 798
suites for a one-helper edit, because nearly every file names a suite
that names another. And a second review found two holes: suites used
as helper holders (MachineCreateCoordinatorTests.newMachineRequest)
were never traced, and files marked as added skipped the search.

scripts/ci/test_impact.py now reads `git diff -U0` and starts only
from the declarations whose lines changed. Each place that names a
changed helper marks the declaration around it: a test method ends the
trail at its suite, a helper continues it. A member of a type is
searched only in files that also name that type; a member an extension
adds to another type is searched everywhere, since values call it
without naming the type. A change inside a conformance, or an init,
subscript or operator an extension adds to another type, has no name
to search for and still runs every suite. Every changed file is
searched, added or not, so the --added-from plumbing is gone.

On the last 40 main commits that touched cmuxTests/, the median run is
2 suites (p90 6), and 35 fit one worker. #14056 selects 24 suites and
its one strict step; #14062 selects 1 suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: close the tracer gaps a third review reproduced

- A helper type's changed member now traces the type, not the member
  name. A mock app code calls (AgentChatResumeIntentRecorder.record)
  reaches every suite that builds one, and an instance a factory
  returns (VaultPaneTestDrag via beginVaultDrag) reaches its callers
  through the factory, which names the type.
- Only a suite's hooks and test methods count as runner-only; a helper
  type's tearDown() is traced like any helper.
- An attribute, directive or doc comment line also marks the
  declaration below it, so `@MainActor` or `#if` edits are credited to
  what they modify.
- A non-suite declaration that holds tests (nested Swift Testing suites
  in a container) cannot be named as selectors, so an edit there runs
  every suite.
- An import or other file-level line changes the whole file.

Both real reproductions from the review now select the missed suites.
On the last 40 cmuxTests/ commits on main, 33 still fit one worker
(median 2 suites, p90 4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Superseded: this fix reached main through #13151 (commit 2346292). Main has a later version of SSHDeepSleepReattachTests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-full-ci Records that skipping the macOS suite on a test-only diff is deliberate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant