Skip to content

Wait for E2E virtual display readiness - #4928

Merged
lawrencecchen merged 1 commit into
mainfrom
feat-ui-test-activation-deflake
May 28, 2026
Merged

lawrencecchen merged 1 commit into
mainfrom
feat-ui-test-activation-deflake

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 28, 2026 •

Copy link
Copy Markdown
Contributor

Replaces the fixed virtual-display sleep in hosted E2E with the helper readiness and display-id markers. The workflow now fails with the helper log if readiness never arrives or the helper exits early.

I tried removing several non-strict UI-test launch expected-failure masks, but the current runner still fails at app.launch() for CommandPalette, UpdatePill, and FeedSidebar. Those masks stay in place. This PR keeps the deterministic CI timing fix only.

Validation:

  • git diff --check
  • actionlint -oneline .github/workflows/test-e2e.yml
  • ./tests/test_ci_self_hosted_guard.sh
  • PR checks pending on final commit

Note

Low Risk
CI-only workflow timing change; no app runtime, auth, or data-path impact.

Overview
Hosted E2E no longer assumes a 3 second delay after starting create-virtual-display. The workflow passes --ready-path and --display-id-path, polls until both marker files exist (or the helper exits), and fails the job with the helper log on timeout or early exit.

That ties test startup to the helper’s real “display created” signal instead of a fixed sleep, which should reduce flaky races when the virtual display is slow or fast to come up.

Reviewed by Cursor Bugbot for commit 0eace82. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented May 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 28, 2026 8:56am
cmux-staging Building Building Preview, Comment May 28, 2026 8:56am

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented May 28, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a shared UI-test launch helper (cmuxLaunchApp) with a direct-process option, updates many UI tests to use it instead of XCTExpectFailure-wrapped launches, wires the helper into the Xcode project, and makes CI virtual-display startup readiness-based.

Changes

UI test launch & CI readiness

Layer / File(s) Summary
Add cmuxLaunchApp and utilities
cmuxUITests/UITestLaunchSupport.swift
Adds cmuxLaunchApp(_:), cmuxLaunchAppProcess, cmuxAppBinaryPath, cmuxUsesDirectProcessLaunch, cmuxActivateAppIfUsingXCTestLaunch, and cmuxPollUntil to support direct-process launches and polling.
Project wiring for helper
cmux.xcodeproj/project.pbxproj
Registers UITestLaunchSupport.swift in the cmuxUITests target sources.
Replace per-test launches with cmuxLaunchApp
cmuxUITests/* (many files)
Multiple test helpers and tests now call cmuxLaunchApp(app) instead of wrapping app.launch() in XCTExpectFailure or calling app.activate() directly; activation polling now uses cmuxActivateAppIfUsingXCTestLaunch and handles direct-process short-circuit paths.
CI virtual display readiness
.github/workflows/test-e2e.yml
Replaces a fixed sleep with readiness/id/log artifacts and polling for the virtual-display helper, failing fast and printing the helper log on premature exit or timeout.

Sequence Diagram

sequenceDiagram
  participant Tester as UI Test
  participant Helper as cmuxLaunchApp
  participant Process as App Binary
  participant App as XCUIApplication
  Tester->>Helper: request app launch with args/env
  Helper->>Process: spawn binary, redirect stdout/stderr to log
  Process->>Helper: write runtime log
  Helper->>App: poll app.state until runningForeground/runningBackground or process exits
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • manaflow-ai/cmux-dev-artifacts#1892 — New launch helpers and test updates touch the same activation paths mentioned in that issue.
  • manaflow-ai/cmux-dev-artifacts#1891 — FeedSidebarUITests activation changes align with the problem described there.
  • manaflow-ai/cmux-dev-artifacts#1913 — HelpMenuUITests launch/activation modifications correspond to this issue.
  • manaflow-ai/cmux-dev-artifacts#1890 — BonsplitTabDragUITests launch/foreground handling was changed and may address that failure.
  • manaflow-ai/cmux-dev-artifacts#1896 — RightSidebarChromeHeightUITests launch/activation changes are relevant to that report.

Poem

🐰 I hopped into tests at dawn,
Replaced fragile starts with steady spawn.
Logs in paw and polls in tune,
CI waits now, no more swoon.
🥕 Ready, set — the tests romp on!

🚥 Pre-merge checks | ✅ 15 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ⚠️ Warning Title describes only the virtual display readiness change; omits the significant UI-test launch infrastructure refactoring and app activation flow changes across 11 test files. Expand title to reflect both changes: e.g., 'Refactor UI test app launch and wait for E2E virtual display readiness' or use a conventional prefix.
Description check ⚠️ Warning The PR description is incomplete. It lacks Testing section details, demo video, and review trigger checklist items required by the template. Add Testing section with testing methodology, Demo Video section (if applicable), include Review Trigger block, and complete the Checklist section with the required confirmations.
✅ Passed checks (15 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PR contains only test-only changes (cmuxUITests directory) and CI/build configuration; no production Swift code modified. Custom check exempts tests from actor isolation review.
Cmux Swift Blocking Runtime ✅ Passed Test-only blocking code: RunLoop polling in UITestLaunchSupport.swift and shell sleep in CI workflows. Deterministic test scaffolding allowed by rule; production code unchanged.
Cmux No Hacky Sleeps ✅ Passed All changes are test-only code (cmuxUITests) or GitHub Actions workflow YAML, both explicitly allowed by the rule. No production runtime code violations.
Cmux Algorithmic Complexity ✅ Passed All changes are test code, CI infrastructure, or build config; no production code with algorithmic complexity issues. Rule explicitly exempts test-only scaffolding and limits scope to production code.
Cmux Swift Concurrency ✅ Passed No legacy async patterns introduced. RunLoop.current.run() in XCTest boundary (UITestLaunchSupport.swift) is allowed per rules. No DispatchQueue.global(), Combine, or fire-and-forget Tasks.
Cmux Swift @Concurrent ✅ Passed PR adds only synchronous UI-test helpers with no async, @concurrent, or nonisolated async declarations. All work is intentional, UI-bound, and compliant with concurrent annotation rules.
Cmux Swift File And Package Boundaries ✅ Passed UITestLaunchSupport.swift (118 lines) is test-target infrastructure code with single responsibility, no mixed responsibilities, qualifying as allowed small test fixture per boundary rules.
Cmux Swift Logging ✅ Passed No logging violations found: no print/debugPrint/dump/NSLog in production code, UITestLaunchSupport.swift is test infrastructure with file logging allowed by rules, no Sources/ modifications.
Cmux User-Facing Error Privacy ✅ Passed PR contains only test code, CI workflows, and build configuration—no production code changes. Check is inapplicable per rule's "Allowed cases: tests" exemption.
Cmux Full Internationalization ✅ Passed PR modifies only test files, test helpers, CI workflow, and project config—no production user-facing strings. Tests are exempted from internationalization per full-internationalization.md.
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI view code changes—only UI test infrastructure, CI configuration, and project build config. UITestLaunchSupport.swift is pure XCTest helper code.
Cmux Architecture Rethink ✅ Passed All changes are test-only, explicitly allowed per rule. Consolidates UI test launch logic into single helper. No production code state owners or duplicate lifecycle ownership.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed All Swift changes are in cmuxUITests/ (test-only), which the rule explicitly allows. UITestLaunchSupport.swift contains no NSWindow/NSPanel/NSWindowController/SwiftUI Window declarations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-ui-test-activation-deflake

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 and usage tips.

@greptile-apps

greptile-apps Bot commented May 28, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Replaces the fixed sleep 3 after starting the virtual display helper with a signal-based polling loop that waits for --ready-path and --display-id-path marker files (up to 10 s / 100 × 0.1 s iterations), and hard-fails with the helper log if the process exits prematurely or the timeout fires. The create-virtual-display.m binary already writes both marker files before entering dispatch_main(), so the contract is fully implemented on both ends.

  • Virtual-display readiness: poll replaces sleep 3; helper's displayIDPath and readyPath writes (lines 229–230 in create-virtual-display.m) are sequential before dispatch_main(), so no race between the two file checks.
  • Failure visibility: helper log is always printed (on timeout and on success), and the workflow step fails with a descriptive message if the helper exits early.
  • FeedSidebarUITests: XCTExpectFailure wrapper around app.launch() removed; launch failures now surface as real test failures.

Confidence Score: 5/5

Safe to merge — only the CI workflow is touched, replacing a fixed sleep with a deterministic readiness handshake that is already fully implemented in the helper binary.

The change is confined to the E2E CI workflow. The create-virtual-display.m binary already writes both marker files (--ready-path and --display-id-path) sequentially before dispatch_main(), so the new polling loop consumes a signal the helper already emits. The 10-second timeout (100 × 0.1 s) is generous for display creation; early-exit and timeout paths both print the helper log and fail the step explicitly. No production app code, auth, or data handling is affected.

No files require special attention.

Important Files Changed

Filename Overview
.github/workflows/test-e2e.yml Replaces fixed 3-second sleep with a readiness-file polling loop (up to 10 s, 0.1 s intervals) that fails fast if the helper exits early; create-virtual-display.m already supports --ready-path and --display-id-path, so the contract is satisfied.

Sequence Diagram

sequenceDiagram
    participant WF as Workflow Step
    participant H as create-virtual-display
    participant FS as Marker Files

    WF->>H: "launch (--ready-path, --display-id-path) &"
    H-->>FS: write displayIDPath
    H-->>FS: write readyPath
    H->>H: dispatch_main() (keeps alive)

    loop Poll up to 100 × 0.1 s
        WF->>FS: "[ -s READY ] && [ -s ID ]"
        alt both non-empty
            WF-->>WF: break
        else helper exited early
            WF-->>WF: cat log, exit 1
        else still waiting
            WF->>WF: sleep 0.1
        end
    end

    WF->>WF: post-loop timeout check (exit 1 if not ready)
    WF->>WF: echo display ID, cat log
Loading

Reviews (8): Last reviewed commit: "Wait for E2E virtual display readiness" | Re-trigger Greptile

Comment thread cmuxUITests/UITestLaunchSupport.swift Outdated
Comment on lines +74 to +91
let testBundle = Bundle(for: UITestLaunchSupportBundleToken.self)
let productsDir = testBundle.bundleURL
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let binaryPath = productsDir
.appendingPathComponent("cmux DEV.app")
.appendingPathComponent("Contents/MacOS/cmux DEV")
.path
if FileManager.default.isExecutableFile(atPath: binaryPath) {
return binaryPath
}

throw NSError(domain: "UITestLaunchSupport", code: 2, userInfo: [
NSLocalizedDescriptionKey: "App binary not found at \(binaryPath). testBundle=\(testBundle.bundleURL.path)"
])
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Binary path traversal is off by one directory level

The four deletingLastPathComponent() calls starting from testBundle.bundleURL (which resolves to the .xctest bundle inside the runner) navigate to .../Build/Products/ rather than .../Build/Products/Debug/. Concretely: .xctest → PlugIns → cmuxUITests-Runner.app → Debug → Products. Appending cmux DEV.app then yields Products/cmux DEV.app/... but the actual artifact lives at Products/Debug/cmux DEV.app/.... Because the workflow sets CMUX_UI_TEST_DIRECT_PROCESS_LAUNCH=1 but does NOT set CMUX_UI_TEST_APP_BINARY_PATH, every CI run will immediately fail with "App binary not found" at the incorrect path. Either drop one deletingLastPathComponent() to land on the configuration directory, or set CMUX_UI_TEST_APP_BINARY_PATH explicitly in the workflow step's env.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
cmuxUITests/WorkspaceSidebarScrollUITests.swift (1)

248-255: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require foreground, not just running/background, before sidebar interaction tests

This helper currently passes when app is backgrounded, but these tests immediately issue keyboard input and visibility assertions that expect an active foreground app.

Suggested fix
 private func launchAndEnsureRunning(_ app: XCUIApplication) {
     cmuxLaunchApp(app)
-    XCTAssertTrue(
-        pollUntil(timeout: 10.0) {
-            app.state == .runningForeground || app.state == .runningBackground
-        },
-        "App failed to launch. state=\(app.state.rawValue)"
-    )
+    XCTAssertTrue(
+        pollUntil(timeout: 10.0) {
+            if app.state != .runningForeground {
+                app.activate()
+            }
+            return app.state == .runningForeground
+        },
+        "App did not reach runningForeground before UI interactions. state=\(app.state.rawValue)"
+    )
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxUITests/WorkspaceSidebarScrollUITests.swift` around lines 248 - 255, The
helper launchAndEnsureRunning currently treats runningBackground as acceptable;
change its readiness check to require the app to be in the .runningForeground
state before proceeding so UI and keyboard interactions are safe: update the
pollUntil predicate in launchAndEnsureRunning (which calls cmuxLaunchApp and
inspects app.state) to return true only when app.state == .runningForeground,
and keep the existing timeout/error message behavior.
cmuxUITests/UpdatePillUITests.swift (1)

278-289: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce foreground before continuing UI interactions

Both launch helpers can continue even if the app never reaches .runningForeground, which weakens activation-failure coverage and can mask regressions behind downstream flake.

Suggested fix
 private func launchAndActivate(_ app: XCUIApplication, activateTimeout: TimeInterval = 2.0) {
     cmuxLaunchApp(app)
     let activated = pollUntil(timeout: activateTimeout) {
         guard app.state != .runningForeground else {
             return true
         }
         app.activate()
         return app.state == .runningForeground
     }
     if !activated {
         app.activate()
     }
+    XCTAssertTrue(
+        pollUntil(timeout: 2.0) { app.state == .runningForeground },
+        "App did not reach runningForeground before UI interactions. state=\(app.state.rawValue)"
+    )
 }
@@
-        _ = pollUntil(timeout: 2.0) {
+        let activated = pollUntil(timeout: 2.0) {
             guard app.state != .runningForeground else {
                 return true
             }
             app.activate()
             return app.state == .runningForeground
         }
+        XCTAssertTrue(
+            activated,
+            "App did not reach runningForeground before titlebar hint assertions. state=\(app.state.rawValue)"
+        )

Also applies to: 383-391

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxUITests/UpdatePillUITests.swift` around lines 278 - 289, The helper
launchAndActivate currently continues even when activation fails; change it to
treat a failed activation as a test failure by replacing the silent fallback
with an explicit assertion/termination (e.g., XCTAssertTrue or XCTFail plus
return/throw) so the test fails if app.state never becomes .runningForeground
after pollUntil; update the same behavior in the other launch helper referenced
(the similar helper at lines ~383-391) to enforce foreground before continuing
UI interactions.
cmuxUITests/BrowserPaneNavigationKeybindUITests.swift (1)

1457-1466: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Foreground helper currently succeeds in background state

launchAndEnsureForeground returns on .runningBackground, which can let keybind tests proceed without an active app and mask launch/activation regressions.

Suggested fix
 private func launchAndEnsureForeground(_ app: XCUIApplication, timeout: TimeInterval = 12.0) {
     cmuxLaunchApp(app)
-
-    if app.state == .runningForeground { return }
-
-    if app.state == .runningBackground {
-        return
-    }
-
-    XCTFail("App failed to start. state=\(app.state.rawValue)")
+    let reachedForeground = waitForCondition(timeout: timeout) {
+        if app.state != .runningForeground {
+            app.activate()
+        }
+        return app.state == .runningForeground
+    }
+    guard reachedForeground else {
+        XCTFail("App did not reach runningForeground before UI interactions. state=\(app.state.rawValue)")
+        return
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxUITests/BrowserPaneNavigationKeybindUITests.swift` around lines 1457 -
1466, The helper launchAndEnsureForeground currently returns when app.state ==
.runningBackground which lets tests proceed with the app in background; update
launchAndEnsureForeground to not treat .runningBackground as success: after
calling cmuxLaunchApp(app) if app.state != .runningForeground then try to bring
it to foreground (call app.activate() and wait/poll for app.state ==
.runningForeground up to the provided timeout) and only then return; if
activation fails within the timeout call XCTFail with the app.state (keep
references to launchAndEnsureForeground, cmuxLaunchApp, XCUIApplication,
app.activate(), app.state == .runningForeground, .runningBackground, and
XCTFail).
cmuxUITests/WorkspaceDescriptionUITests.swift (1)

275-282: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

launchAndEnsureForeground should not accept background as success

This helper currently returns when state is .runningBackground, so tests may proceed before the app is interactable in foreground.

Suggested fix
 private func launchAndEnsureForeground(_ app: XCUIApplication, timeout: TimeInterval = 12.0) {
     cmuxLaunchApp(app)
-
-    if app.state == .runningForeground { return }
-    if app.state == .runningBackground { return }
-
-    XCTFail("App failed to start. state=\(app.state.rawValue)")
+    let reachedForeground = workspaceDescriptionPollUntil(timeout: timeout) {
+        if app.state != .runningForeground {
+            app.activate()
+        }
+        return app.state == .runningForeground
+    }
+    guard reachedForeground else {
+        XCTFail("App did not reach runningForeground before UI interactions. state=\(app.state.rawValue)")
+        return
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxUITests/WorkspaceDescriptionUITests.swift` around lines 275 - 282, The
helper launchAndEnsureForeground currently treats .runningBackground as a
success and can allow tests to continue before the app is interactable; change
launchAndEnsureForeground (which calls cmuxLaunchApp) to treat only
.runningForeground as success: remove the early return for .runningBackground
and instead wait (via a short poll loop or an XCTNSPredicateExpectation on
app.state == .runningForeground) up to the timeout, then XCTFail if the app
never reaches .runningForeground; ensure any waiting uses the provided timeout
parameter and preserves the existing XCTFail message including
app.state.rawValue.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@cmuxUITests/BrowserPaneNavigationKeybindUITests.swift`:
- Around line 1457-1466: The helper launchAndEnsureForeground currently returns
when app.state == .runningBackground which lets tests proceed with the app in
background; update launchAndEnsureForeground to not treat .runningBackground as
success: after calling cmuxLaunchApp(app) if app.state != .runningForeground
then try to bring it to foreground (call app.activate() and wait/poll for
app.state == .runningForeground up to the provided timeout) and only then
return; if activation fails within the timeout call XCTFail with the app.state
(keep references to launchAndEnsureForeground, cmuxLaunchApp, XCUIApplication,
app.activate(), app.state == .runningForeground, .runningBackground, and
XCTFail).

In `@cmuxUITests/UpdatePillUITests.swift`:
- Around line 278-289: The helper launchAndActivate currently continues even
when activation fails; change it to treat a failed activation as a test failure
by replacing the silent fallback with an explicit assertion/termination (e.g.,
XCTAssertTrue or XCTFail plus return/throw) so the test fails if app.state never
becomes .runningForeground after pollUntil; update the same behavior in the
other launch helper referenced (the similar helper at lines ~383-391) to enforce
foreground before continuing UI interactions.

In `@cmuxUITests/WorkspaceDescriptionUITests.swift`:
- Around line 275-282: The helper launchAndEnsureForeground currently treats
.runningBackground as a success and can allow tests to continue before the app
is interactable; change launchAndEnsureForeground (which calls cmuxLaunchApp) to
treat only .runningForeground as success: remove the early return for
.runningBackground and instead wait (via a short poll loop or an
XCTNSPredicateExpectation on app.state == .runningForeground) up to the timeout,
then XCTFail if the app never reaches .runningForeground; ensure any waiting
uses the provided timeout parameter and preserves the existing XCTFail message
including app.state.rawValue.

In `@cmuxUITests/WorkspaceSidebarScrollUITests.swift`:
- Around line 248-255: The helper launchAndEnsureRunning currently treats
runningBackground as acceptable; change its readiness check to require the app
to be in the .runningForeground state before proceeding so UI and keyboard
interactions are safe: update the pollUntil predicate in launchAndEnsureRunning
(which calls cmuxLaunchApp and inspects app.state) to return true only when
app.state == .runningForeground, and keep the existing timeout/error message
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b5d701aa-2e4e-4f89-9d7b-257847153190

📥 Commits

Reviewing files that changed from the base of the PR and between fe73d5f and 0bea59b.

📒 Files selected for processing (15)
  • .github/workflows/test-e2e.yml
  • cmux.xcodeproj/project.pbxproj
  • cmuxUITests/BonsplitTabDragUITests.swift
  • cmuxUITests/BrowserPaneNavigationKeybindUITests.swift
  • cmuxUITests/CommandPaletteIdentifierClipboardUITests.swift
  • cmuxUITests/FeedSidebarUITests.swift
  • cmuxUITests/FindSelectionShortcutUITests.swift
  • cmuxUITests/HelpMenuUITests.swift
  • cmuxUITests/RightSidebarChromeHeightUITests.swift
  • cmuxUITests/SidebarHelpMenuUITests.swift
  • cmuxUITests/TerminalCmdClickUITests.swift
  • cmuxUITests/UITestLaunchSupport.swift
  • cmuxUITests/UpdatePillUITests.swift
  • cmuxUITests/WorkspaceDescriptionUITests.swift
  • cmuxUITests/WorkspaceSidebarScrollUITests.swift

@lawrencecchen
lawrencecchen force-pushed the feat-ui-test-activation-deflake branch from 0bea59b to 25d4620 Compare May 28, 2026 07:15
Comment thread cmuxUITests/UITestLaunchSupport.swift Outdated
@lawrencecchen
lawrencecchen force-pushed the feat-ui-test-activation-deflake branch from 25d4620 to 0c211cb Compare May 28, 2026 07:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxUITests/UITestLaunchSupport.swift`:
- Around line 12-15: The current guard uses the wrong env var and makes
direct-process launch the default; update the condition to check
ProcessInfo.processInfo.environment["CMUX_UI_TEST_DIRECT_PROCESS_LAUNCH"] == "1"
and only take the direct path (the code that bypasses app.launch()) when that
opt-in variable is set; otherwise call app.launch() as the default. Ensure you
replace any reference to "CMUX_UI_TEST_XCTEST_LAUNCH" with
"CMUX_UI_TEST_DIRECT_PROCESS_LAUNCH" and keep the calls to app.launch() and the
direct-launch branch intact (refer to ProcessInfo.processInfo.environment and
app.launch()).
- Around line 25-28: The helper currently calls app.terminate() and ignores the
boolean result of cmuxPollUntil(timeout: 3.0) { app.state == .notRunning }, so a
stale app instance can persist; update the helper (the block invoking
app.terminate() and cmuxPollUntil) to check the return value and fail fast when
it returns false—e.g., assert/XCTFail or throw/fatalError with a clear message
if cmuxPollUntil(...) returns false indicating the previous instance did not
exit—so the new launch is aborted instead of proceeding.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 775e4c82-4301-4a3e-86b0-83e2d26bc3dd

📥 Commits

Reviewing files that changed from the base of the PR and between 0bea59b and 25d4620.

📒 Files selected for processing (15)
  • .github/workflows/test-e2e.yml
  • cmux.xcodeproj/project.pbxproj
  • cmuxUITests/BonsplitTabDragUITests.swift
  • cmuxUITests/BrowserPaneNavigationKeybindUITests.swift
  • cmuxUITests/CommandPaletteIdentifierClipboardUITests.swift
  • cmuxUITests/FeedSidebarUITests.swift
  • cmuxUITests/FindSelectionShortcutUITests.swift
  • cmuxUITests/HelpMenuUITests.swift
  • cmuxUITests/RightSidebarChromeHeightUITests.swift
  • cmuxUITests/SidebarHelpMenuUITests.swift
  • cmuxUITests/TerminalCmdClickUITests.swift
  • cmuxUITests/UITestLaunchSupport.swift
  • cmuxUITests/UpdatePillUITests.swift
  • cmuxUITests/WorkspaceDescriptionUITests.swift
  • cmuxUITests/WorkspaceSidebarScrollUITests.swift

Comment thread cmuxUITests/UITestLaunchSupport.swift Outdated
Comment on lines +12 to +15
guard ProcessInfo.processInfo.environment["CMUX_UI_TEST_XCTEST_LAUNCH"] != "1" else {
app.launch()
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the documented opt-in flag for direct launch.

Lines 12-15 currently make direct-process launch the default and key it off CMUX_UI_TEST_XCTEST_LAUNCH, but the PR objective says CI should opt in with CMUX_UI_TEST_DIRECT_PROCESS_LAUNCH=1 and local runs should stay on app.launch(). As written, local runs will take the direct path and the documented CI flag is ignored.

Suggested fix
-    guard ProcessInfo.processInfo.environment["CMUX_UI_TEST_XCTEST_LAUNCH"] != "1" else {
-        app.launch()
-        return
-    }
+    guard ProcessInfo.processInfo.environment["CMUX_UI_TEST_DIRECT_PROCESS_LAUNCH"] == "1" else {
+        app.launch()
+        return
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
guard ProcessInfo.processInfo.environment["CMUX_UI_TEST_XCTEST_LAUNCH"] != "1" else {
app.launch()
return
}
guard ProcessInfo.processInfo.environment["CMUX_UI_TEST_DIRECT_PROCESS_LAUNCH"] == "1" else {
app.launch()
return
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxUITests/UITestLaunchSupport.swift` around lines 12 - 15, The current
guard uses the wrong env var and makes direct-process launch the default; update
the condition to check
ProcessInfo.processInfo.environment["CMUX_UI_TEST_DIRECT_PROCESS_LAUNCH"] == "1"
and only take the direct path (the code that bypasses app.launch()) when that
opt-in variable is set; otherwise call app.launch() as the default. Ensure you
replace any reference to "CMUX_UI_TEST_XCTEST_LAUNCH" with
"CMUX_UI_TEST_DIRECT_PROCESS_LAUNCH" and keep the calls to app.launch() and the
direct-launch branch intact (refer to ProcessInfo.processInfo.environment and
app.launch()).

Comment thread cmuxUITests/UITestLaunchSupport.swift Outdated
Comment on lines +25 to +28
app.terminate()
_ = cmuxPollUntil(timeout: 3.0) {
app.state == .notRunning
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast if the previous app instance never exits.

Lines 25-28 ignore the timeout result and continue launching a new process even when the old instance is still running. That keeps the stale-instance race alive in the exact path this helper is supposed to deflake.

Suggested fix
     app.terminate()
-    _ = cmuxPollUntil(timeout: 3.0) {
+    let didTerminate = cmuxPollUntil(timeout: 3.0) {
         app.state == .notRunning
     }
+    guard didTerminate else {
+        throw NSError(domain: "UITestLaunchSupport", code: 3, userInfo: [
+            NSLocalizedDescriptionKey: "Existing app instance did not terminate before relaunch."
+        ])
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.terminate()
_ = cmuxPollUntil(timeout: 3.0) {
app.state == .notRunning
}
app.terminate()
let didTerminate = cmuxPollUntil(timeout: 3.0) {
app.state == .notRunning
}
guard didTerminate else {
throw NSError(domain: "UITestLaunchSupport", code: 3, userInfo: [
NSLocalizedDescriptionKey: "Existing app instance did not terminate before relaunch."
])
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxUITests/UITestLaunchSupport.swift` around lines 25 - 28, The helper
currently calls app.terminate() and ignores the boolean result of
cmuxPollUntil(timeout: 3.0) { app.state == .notRunning }, so a stale app
instance can persist; update the helper (the block invoking app.terminate() and
cmuxPollUntil) to check the return value and fail fast when it returns
false—e.g., assert/XCTFail or throw/fatalError with a clear message if
cmuxPollUntil(...) returns false indicating the previous instance did not
exit—so the new launch is aborted instead of proceeding.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0c211cb. Configure here.

Comment thread cmuxUITests/BonsplitTabDragUITests.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxUITests/BrowserPaneNavigationKeybindUITests.swift`:
- Around line 1462-1464: The early return in BrowserPaneNavigationKeybindUITests
that does "if app.state == .runningBackground { return }" lets XCTest-launched
runs skip activation and makes keyboard events flaky; update the guard to only
return for direct-process launches by adding a check for XCTest test-run
indicators (e.g.
ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] or
ProcessInfo.processInfo.arguments contains "XCTest") so the condition becomes:
if app.state == .runningBackground &&
ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil {
return } (locate and modify this conditional in
BrowserPaneNavigationKeybindUITests.swift where app.state is checked).

In `@cmuxUITests/SidebarHelpMenuUITests.swift`:
- Around line 850-857: The current check only treats .runningForeground as
success inside sidebarHelpPollUntil, which causes spurious failures when the app
transitions to .runningBackground under direct-process launch; update the poll
closure used by sidebarHelpPollUntil so it returns true when app.state is either
.runningForeground or .runningBackground (i.e., treat both states as success),
and keep calling cmuxActivateAppIfUsingXCTestLaunch(app) only when neither state
is present; reference the existing symbols cmuxUsesDirectProcessLaunch(),
app.state, sidebarHelpPollUntil, and cmuxActivateAppIfUsingXCTestLaunch to
locate and change the closure logic accordingly.

In `@cmuxUITests/UITestLaunchSupport.swift`:
- Around line 84-99: The resolver currently only checks for the Debug product
name "cmux DEV.app" (binaryPath) and throws if not found; update the logic in
UITestLaunchSupport (around UITestLaunchSupportBundleToken and the binaryPath
resolution) to also probe the Release product name ("cmux.app" /
"Contents/MacOS/cmux") before failing, and fall back to the existing
CMUX_UI_TEST_APP_BINARY_PATH env var if present; if neither binary is
executable, keep throwing the NSError with the same descriptive message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1bc8f528-05ec-4201-aec0-1a4be711d276

📥 Commits

Reviewing files that changed from the base of the PR and between 25d4620 and 0c211cb.

📒 Files selected for processing (15)
  • .github/workflows/test-e2e.yml
  • cmux.xcodeproj/project.pbxproj
  • cmuxUITests/BonsplitTabDragUITests.swift
  • cmuxUITests/BrowserPaneNavigationKeybindUITests.swift
  • cmuxUITests/CommandPaletteIdentifierClipboardUITests.swift
  • cmuxUITests/FeedSidebarUITests.swift
  • cmuxUITests/FindSelectionShortcutUITests.swift
  • cmuxUITests/HelpMenuUITests.swift
  • cmuxUITests/RightSidebarChromeHeightUITests.swift
  • cmuxUITests/SidebarHelpMenuUITests.swift
  • cmuxUITests/TerminalCmdClickUITests.swift
  • cmuxUITests/UITestLaunchSupport.swift
  • cmuxUITests/UpdatePillUITests.swift
  • cmuxUITests/WorkspaceDescriptionUITests.swift
  • cmuxUITests/WorkspaceSidebarScrollUITests.swift

Comment on lines 1462 to 1464
if app.state == .runningBackground {
// App launched but couldn't activate — continue in background.
// XCUIElement queries and keyboard input work through the
// accessibility framework regardless of activation state.
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t return immediately on background state for XCTest launch mode.

Line 1462-1464 exits before attempting activation, so tests can continue while the app is backgrounded and keyboard events become unreliable. Gate this early return to direct-process launch only.

Proposed fix
 private func launchAndEnsureForeground(_ app: XCUIApplication, timeout: TimeInterval = 12.0) {
     cmuxLaunchApp(app)

     if app.state == .runningForeground { return }

     if app.state == .runningBackground {
-        return
+        if cmuxUsesDirectProcessLaunch() { return }
+        cmuxActivateAppIfUsingXCTestLaunch(app)
+        if app.wait(for: .runningForeground, timeout: 6.0) { return }
     }

     XCTFail("App failed to start. state=\(app.state.rawValue)")
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxUITests/BrowserPaneNavigationKeybindUITests.swift` around lines 1462 -
1464, The early return in BrowserPaneNavigationKeybindUITests that does "if
app.state == .runningBackground { return }" lets XCTest-launched runs skip
activation and makes keyboard events flaky; update the guard to only return for
direct-process launches by adding a check for XCTest test-run indicators (e.g.
ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] or
ProcessInfo.processInfo.arguments contains "XCTest") so the condition becomes:
if app.state == .runningBackground &&
ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil {
return } (locate and modify this conditional in
BrowserPaneNavigationKeybindUITests.swift where app.state is checked).

Comment on lines 850 to 857
if cmuxUsesDirectProcessLaunch(), app.state == .runningForeground || app.state == .runningBackground {
return
}
XCTAssertTrue(
sidebarHelpPollUntil(timeout: 4.0) {
guard app.state != .runningForeground else { return true }
app.activate()
cmuxActivateAppIfUsingXCTestLaunch(app)
return app.state == .runningForeground

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle delayed state transitions in direct-process launch path.

Line 850-857 checks direct-process runnable state only once. If state becomes .runningBackground slightly later, the code falls into a foreground-only poll where activation is a no-op and can fail spuriously.

Proposed fix
 private func launchAndActivate(_ app: XCUIApplication) {
     cmuxLaunchApp(app)
-    if cmuxUsesDirectProcessLaunch(), app.state == .runningForeground || app.state == .runningBackground {
-        return
+    if cmuxUsesDirectProcessLaunch() {
+        XCTAssertTrue(
+            sidebarHelpPollUntil(timeout: 4.0) {
+                app.state == .runningForeground || app.state == .runningBackground
+            },
+            "App did not reach a runnable state before UI interactions"
+        )
+        return
     }
     XCTAssertTrue(
         sidebarHelpPollUntil(timeout: 4.0) {
             guard app.state != .runningForeground else { return true }
             cmuxActivateAppIfUsingXCTestLaunch(app)
             return app.state == .runningForeground
         },
         "App did not reach runningForeground before UI interactions"
     )
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxUITests/SidebarHelpMenuUITests.swift` around lines 850 - 857, The current
check only treats .runningForeground as success inside sidebarHelpPollUntil,
which causes spurious failures when the app transitions to .runningBackground
under direct-process launch; update the poll closure used by
sidebarHelpPollUntil so it returns true when app.state is either
.runningForeground or .runningBackground (i.e., treat both states as success),
and keep calling cmuxActivateAppIfUsingXCTestLaunch(app) only when neither state
is present; reference the existing symbols cmuxUsesDirectProcessLaunch(),
app.state, sidebarHelpPollUntil, and cmuxActivateAppIfUsingXCTestLaunch to
locate and change the closure logic accordingly.

Comment thread cmuxUITests/UITestLaunchSupport.swift Outdated
Comment on lines +84 to +99
let testBundle = Bundle(for: UITestLaunchSupportBundleToken.self)
let productsDir = testBundle.bundleURL
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let binaryPath = productsDir
.appendingPathComponent("cmux DEV.app")
.appendingPathComponent("Contents/MacOS/cmux DEV")
.path
if FileManager.default.isExecutableFile(atPath: binaryPath) {
return binaryPath
}

throw NSError(domain: "UITestLaunchSupport", code: 2, userInfo: [
NSLocalizedDescriptionKey: "App binary not found at \(binaryPath). testBundle=\(testBundle.bundleURL.path)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle both Debug and Release app product names.

This resolver only probes cmux DEV.app, but cmux.xcodeproj/project.pbxproj builds the app as cmux DEV in Debug and cmux in Release. Direct launch will therefore fail under the cmuxUITests Release config unless CMUX_UI_TEST_APP_BINARY_PATH is set manually.

Suggested fix
-    let binaryPath = productsDir
-        .appendingPathComponent("cmux DEV.app")
-        .appendingPathComponent("Contents/MacOS/cmux DEV")
-        .path
-    if FileManager.default.isExecutableFile(atPath: binaryPath) {
-        return binaryPath
+    let candidates = [
+        ("cmux DEV.app", "cmux DEV"),
+        ("cmux.app", "cmux"),
+    ]
+    for (appName, executableName) in candidates {
+        let binaryPath = productsDir
+            .appendingPathComponent(appName)
+            .appendingPathComponent("Contents/MacOS/\(executableName)")
+            .path
+        if FileManager.default.isExecutableFile(atPath: binaryPath) {
+            return binaryPath
+        }
     }
 
     throw NSError(domain: "UITestLaunchSupport", code: 2, userInfo: [
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxUITests/UITestLaunchSupport.swift` around lines 84 - 99, The resolver
currently only checks for the Debug product name "cmux DEV.app" (binaryPath) and
throws if not found; update the logic in UITestLaunchSupport (around
UITestLaunchSupportBundleToken and the binaryPath resolution) to also probe the
Release product name ("cmux.app" / "Contents/MacOS/cmux") before failing, and
fall back to the existing CMUX_UI_TEST_APP_BINARY_PATH env var if present; if
neither binary is executable, keep throwing the NSError with the same
descriptive message.

@lawrencecchen
lawrencecchen force-pushed the feat-ui-test-activation-deflake branch from 0c211cb to 6893b1e Compare May 28, 2026 07:41
@lawrencecchen
lawrencecchen force-pushed the feat-ui-test-activation-deflake branch from 6893b1e to 62b7da7 Compare May 28, 2026 08:14
@lawrencecchen lawrencecchen changed the title Restore UI activation failure coverage Tighten E2E launch readiness May 28, 2026
@lawrencecchen
lawrencecchen force-pushed the feat-ui-test-activation-deflake branch from 62b7da7 to 0eace82 Compare May 28, 2026 08:43
@lawrencecchen lawrencecchen changed the title Tighten E2E launch readiness Wait for E2E virtual display readiness May 28, 2026
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​postgres@​3.4.99910010084100
Addednpm/​pg@​8.20.0991009989100

View full report

@lawrencecchen
lawrencecchen merged commit 95d4c2f into main May 28, 2026
19 checks passed
@lawrencecchen
lawrencecchen deleted the feat-ui-test-activation-deflake branch May 28, 2026 09:04
hhsw2015 pushed a commit to hhsw2015/cmux that referenced this pull request May 28, 2026
- a791621 Reduce browser WebView input latency (manaflow-ai#4863)
- f2dbc31 Fix file preview Open With menu (manaflow-ai#4932)
- 95d4c2f Wait for E2E virtual display readiness (manaflow-ai#4928)
- 86558d1 Add cmux diff CodeView command (manaflow-ai#4451)
- 6dcddde Run E2E xcodebuild in GUI bootstrap (manaflow-ai#4940)

Conflicts resolved:
- CLINotifyProcessIntegrationRegressionTests.swift: kept both fork and upstream test additions.
- Workspace.swift: kept fork's per-layoutTab snapshot logic; rawLayout aliases the selected tab's layout to satisfy upstream's downstream uses.

This branch was successfully deployed

1 active deployment
Preview – cmux — 0eace829 Deployed May 28, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant