From b1e2d1cb1964aa32b8ef9cecfcf5ce9a3f49a3da Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 01:40:04 -0700 Subject: [PATCH 1/6] Pass display harness manifest path into UI test --- .github/workflows/ci.yml | 1 + .github/workflows/test-e2e.yml | 1 + cmuxUITests/DisplayResolutionRegressionUITests.swift | 9 +++++---- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e93274d3..04849f71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -513,6 +513,7 @@ jobs: CMUX_UI_TEST_DISPLAY_START_PATH="$START_PATH" \ CMUX_UI_TEST_DISPLAY_DONE_PATH="$DONE_PATH" \ CMUX_UI_TEST_DISPLAY_LOG_PATH="$LOG_PATH" \ + CMUX_UI_TEST_DISPLAY_HARNESS_MANIFEST_PATH="$MANIFEST_PATH" \ xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \ -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ -disableAutomaticPackageResolution \ diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 6f651725..c56f833e 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -257,6 +257,7 @@ jobs: CMUX_UI_TEST_DISPLAY_START_PATH="$START_PATH" CMUX_UI_TEST_DISPLAY_DONE_PATH="$DONE_PATH" CMUX_UI_TEST_DISPLAY_LOG_PATH="$LOG_PATH" + CMUX_UI_TEST_DISPLAY_HARNESS_MANIFEST_PATH="$MANIFEST_PATH" ) fi diff --git a/cmuxUITests/DisplayResolutionRegressionUITests.swift b/cmuxUITests/DisplayResolutionRegressionUITests.swift index 579ae221..f508e09f 100644 --- a/cmuxUITests/DisplayResolutionRegressionUITests.swift +++ b/cmuxUITests/DisplayResolutionRegressionUITests.swift @@ -2,7 +2,7 @@ import XCTest import Foundation final class DisplayResolutionRegressionUITests: XCTestCase { - private let displayHarnessManifestPath = "/tmp/cmux-ui-test-display-harness.json" + private let defaultDisplayHarnessManifestPath = "/tmp/cmux-ui-test-display-harness.json" private var launchTag = "" private var diagnosticsPath = "" private var displayReadyPath = "" @@ -118,7 +118,7 @@ final class DisplayResolutionRegressionUITests: XCTestCase { private func prepareDisplayHarnessIfNeeded() throws { let env = ProcessInfo.processInfo.environment - if let externalHarness = loadExternalHarnessFromEnvironment(env) ?? loadExternalHarnessFromManifest() { + if let externalHarness = loadExternalHarnessFromEnvironment(env) ?? loadExternalHarnessFromManifest(env) { displayReadyPath = externalHarness.readyPath displayIDPath = externalHarness.displayIDPath displayStartPath = externalHarness.startPath @@ -150,8 +150,9 @@ final class DisplayResolutionRegressionUITests: XCTestCase { ) } - private func loadExternalHarnessFromManifest() -> ExternalDisplayHarness? { - let manifestURL = URL(fileURLWithPath: displayHarnessManifestPath) + private func loadExternalHarnessFromManifest(_ env: [String: String]) -> ExternalDisplayHarness? { + let manifestPath = env["CMUX_UI_TEST_DISPLAY_HARNESS_MANIFEST_PATH"] ?? defaultDisplayHarnessManifestPath + let manifestURL = URL(fileURLWithPath: manifestPath) guard let data = try? Data(contentsOf: manifestURL) else { return nil } From 95cd26c0b6a850892ae68c172339ca77025e15af Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 01:46:32 -0700 Subject: [PATCH 2/6] Restore stable display harness manifest path --- .github/workflows/ci.yml | 2 +- .github/workflows/test-e2e.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04849f71..fc905d66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -473,7 +473,7 @@ jobs: START_PATH="${PREFIX}.start" DONE_PATH="${PREFIX}.done" LOG_PATH="${PREFIX}.log" - MANIFEST_PATH="${HARNESS_DIR}/cmux-ui-test-display-harness.json" + MANIFEST_PATH="/tmp/cmux-ui-test-display-harness.json" rm -f "$READY_PATH" "$DISPLAY_ID_PATH" "$START_PATH" "$DONE_PATH" "$LOG_PATH" "$MANIFEST_PATH" diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index c56f833e..7e5105ae 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -216,7 +216,7 @@ jobs: START_PATH="${PREFIX}.start" DONE_PATH="${PREFIX}.done" LOG_PATH="${PREFIX}.log" - MANIFEST_PATH="${HARNESS_DIR}/cmux-ui-test-display-harness.json" + MANIFEST_PATH="/tmp/cmux-ui-test-display-harness.json" rm -f "$READY_PATH" "$DISPLAY_ID_PATH" "$START_PATH" "$DONE_PATH" "$LOG_PATH" "$MANIFEST_PATH" From 1c6d5568f7a25d3e4a3c98b848f6bb1b89e5e6ba Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 03:13:23 -0700 Subject: [PATCH 3/6] Move display harness rendezvous files to /tmp --- .github/workflows/ci.yml | 4 +--- .github/workflows/test-e2e.yml | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc905d66..d1fdec6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -465,9 +465,7 @@ jobs: run: | set -euo pipefail SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" - HARNESS_DIR="${RUNNER_TEMP}/cmux-display-churn-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - mkdir -p "$HARNESS_DIR" - PREFIX="${HARNESS_DIR}/cmux-display-churn" + PREFIX="/tmp/cmux-display-churn-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" READY_PATH="${PREFIX}.ready" DISPLAY_ID_PATH="${PREFIX}.id" START_PATH="${PREFIX}.start" diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 7e5105ae..e538b6c5 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -208,9 +208,7 @@ jobs: DISPLAY_ENV_PREFIX=() if [ "$TEST_FILTER" = "DisplayResolutionRegressionUITests" ]; then - HARNESS_DIR="${RUNNER_TEMP}/cmux-display-churn-${{ github.run_id }}-${{ github.run_attempt }}" - mkdir -p "$HARNESS_DIR" - PREFIX="${HARNESS_DIR}/cmux-display-churn" + PREFIX="/tmp/cmux-display-churn-${{ github.run_id }}-${{ github.run_attempt }}" READY_PATH="${PREFIX}.ready" DISPLAY_ID_PATH="${PREFIX}.id" START_PATH="${PREFIX}.start" From 7fb1f50966def2a4d37a7f7c9c62f967b28c4592 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 03:22:04 -0700 Subject: [PATCH 4/6] Launch display helper from inside the UI test sandbox --- .github/workflows/ci.yml | 47 ++----------------- .github/workflows/test-e2e.yml | 47 ++----------------- .../DisplayResolutionRegressionUITests.swift | 28 ++++++++--- 3 files changed, 28 insertions(+), 94 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1fdec6d..8018dbf9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -465,53 +465,12 @@ jobs: run: | set -euo pipefail SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" - PREFIX="/tmp/cmux-display-churn-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - READY_PATH="${PREFIX}.ready" - DISPLAY_ID_PATH="${PREFIX}.id" - START_PATH="${PREFIX}.start" - DONE_PATH="${PREFIX}.done" - LOG_PATH="${PREFIX}.log" - MANIFEST_PATH="/tmp/cmux-ui-test-display-harness.json" - - rm -f "$READY_PATH" "$DISPLAY_ID_PATH" "$START_PATH" "$DONE_PATH" "$LOG_PATH" "$MANIFEST_PATH" + HELPER_PATH="/tmp/create-virtual-display" clang -framework Foundation -framework CoreGraphics \ - -o /tmp/create-virtual-display scripts/create-virtual-display.m + -o "$HELPER_PATH" scripts/create-virtual-display.m - /tmp/create-virtual-display \ - --modes 1920x1080,1728x1117,1600x900,1440x810 \ - --ready-path "$READY_PATH" \ - --display-id-path "$DISPLAY_ID_PATH" \ - --start-path "$START_PATH" \ - --done-path "$DONE_PATH" \ - --iterations 40 \ - --interval-ms 40 \ - >"$LOG_PATH" 2>&1 & - VDISPLAY_PID=$! - trap 'kill "$VDISPLAY_PID" >/dev/null 2>&1 || true; rm -f "$MANIFEST_PATH"' EXIT - - for _ in {1..120}; do - [ -f "$READY_PATH" ] && break - sleep 0.25 - done - [ -f "$READY_PATH" ] || { - echo "Display harness failed to start" >&2 - cat "$LOG_PATH" >&2 || true - exit 1 - } - - cat >"$MANIFEST_PATH" <"$LOG_PATH" 2>&1 & - DISPLAY_VDISPLAY_PID=$! - trap 'kill "${DISPLAY_VDISPLAY_PID:-}" >/dev/null 2>&1 || true; rm -f "$MANIFEST_PATH"' EXIT - - for _ in {1..120}; do - [ -f "$READY_PATH" ] && break - sleep 0.25 - done - [ -f "$READY_PATH" ] || { - echo "Display harness failed to start" >&2 - cat "$LOG_PATH" >&2 || true - exit 1 - } - - cat >"$MANIFEST_PATH" < String? { + guard let helperBinaryPath = env["CMUX_UI_TEST_DISPLAY_HELPER_BINARY_PATH"], + !helperBinaryPath.isEmpty else { + return nil + } + return helperBinaryPath + } + private func loadExternalHarnessFromEnvironment(_ env: [String: String]) -> ExternalDisplayHarness? { guard let readyPath = env["CMUX_UI_TEST_DISPLAY_READY_PATH"], !readyPath.isEmpty, let displayIDPath = env["CMUX_UI_TEST_DISPLAY_ID_PATH"], !displayIDPath.isEmpty, From fbe209cb33daf1007087f11068b85a32e06c04e8 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Wed, 18 Mar 2026 03:30:32 -0700 Subject: [PATCH 5/6] Read the prebuilt display helper path from the harness manifest --- .github/workflows/ci.yml | 9 ++++- .github/workflows/test-e2e.yml | 10 ++++-- .../DisplayResolutionRegressionUITests.swift | 33 ++++++++++++++----- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8018dbf9..78dea457 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -466,11 +466,18 @@ jobs: set -euo pipefail SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" HELPER_PATH="/tmp/create-virtual-display" + MANIFEST_PATH="/tmp/cmux-ui-test-display-harness.json" + + rm -f "$MANIFEST_PATH" + trap 'rm -f "$MANIFEST_PATH"' EXIT clang -framework Foundation -framework CoreGraphics \ -o "$HELPER_PATH" scripts/create-virtual-display.m - CMUX_UI_TEST_DISPLAY_HELPER_BINARY_PATH="$HELPER_PATH" \ + cat >"$MANIFEST_PATH" <"$MANIFEST_PATH" < Date: Wed, 18 Mar 2026 03:49:24 -0700 Subject: [PATCH 6/6] Fix test target build after split test sync --- .../AppDelegateShortcutRoutingTests.swift | 10 ++--- cmuxTests/BrowserConfigTests.swift | 37 ------------------- cmuxTests/BrowserPanelTests.swift | 8 ++-- cmuxTests/WindowAndDragTests.swift | 37 +++++++++++++++++++ cmuxTests/WorkspaceManualUnreadTests.swift | 3 +- cmuxTests/WorkspaceUnitTests.swift | 1 - 6 files changed, 48 insertions(+), 48 deletions(-) diff --git a/cmuxTests/AppDelegateShortcutRoutingTests.swift b/cmuxTests/AppDelegateShortcutRoutingTests.swift index 8bc58a59..efb38674 100644 --- a/cmuxTests/AppDelegateShortcutRoutingTests.swift +++ b/cmuxTests/AppDelegateShortcutRoutingTests.swift @@ -6,7 +6,7 @@ import XCTest @testable import cmux #endif -private let lastSurfaceCloseShortcutDefaultsKey = "closeWorkspaceOnLastSurfaceShortcut" +private let appDelegateLastSurfaceCloseShortcutDefaultsKey = "closeWorkspaceOnLastSurfaceShortcut" @MainActor final class AppDelegateShortcutRoutingTests: XCTestCase { @@ -725,13 +725,13 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { } let defaults = UserDefaults.standard - let originalSetting = defaults.object(forKey: lastSurfaceCloseShortcutDefaultsKey) - defaults.set(false, forKey: lastSurfaceCloseShortcutDefaultsKey) + let originalSetting = defaults.object(forKey: appDelegateLastSurfaceCloseShortcutDefaultsKey) + defaults.set(false, forKey: appDelegateLastSurfaceCloseShortcutDefaultsKey) defer { if let originalSetting { - defaults.set(originalSetting, forKey: lastSurfaceCloseShortcutDefaultsKey) + defaults.set(originalSetting, forKey: appDelegateLastSurfaceCloseShortcutDefaultsKey) } else { - defaults.removeObject(forKey: lastSurfaceCloseShortcutDefaultsKey) + defaults.removeObject(forKey: appDelegateLastSurfaceCloseShortcutDefaultsKey) } } diff --git a/cmuxTests/BrowserConfigTests.swift b/cmuxTests/BrowserConfigTests.swift index 487c680c..b60ccef8 100644 --- a/cmuxTests/BrowserConfigTests.swift +++ b/cmuxTests/BrowserConfigTests.swift @@ -2737,43 +2737,6 @@ final class CmuxWebViewDragRoutingTests: XCTestCase { } } -#if compiler(>=6.2) -@available(macOS 26.0, *) -private struct DragConfigurationOperationsSnapshot: Equatable { - let allowCopy: Bool - let allowMove: Bool - let allowDelete: Bool - let allowAlias: Bool -} - -@available(macOS 26.0, *) -private enum DragConfigurationSnapshotError: Error { - case missingBoolField(primary: String, fallback: String?) -} - -@available(macOS 26.0, *) -private func dragConfigurationOperationsSnapshot(from operations: T) throws -> DragConfigurationOperationsSnapshot { - let mirror = Mirror(reflecting: operations) - - func readBool(_ primary: String, fallback: String? = nil) throws -> Bool { - if let value = mirror.descendant(primary) as? Bool { - return value - } - if let fallback, let value = mirror.descendant(fallback) as? Bool { - return value - } - throw DragConfigurationSnapshotError.missingBoolField(primary: primary, fallback: fallback) - } - - return try DragConfigurationOperationsSnapshot( - allowCopy: readBool("allowCopy", fallback: "_allowCopy"), - allowMove: readBool("allowMove", fallback: "_allowMove"), - allowDelete: readBool("allowDelete", fallback: "_allowDelete"), - allowAlias: readBool("allowAlias", fallback: "_allowAlias") - ) -} - - final class BrowserLinkOpenSettingsTests: XCTestCase { private var suiteName: String! private var defaults: UserDefaults! diff --git a/cmuxTests/BrowserPanelTests.swift b/cmuxTests/BrowserPanelTests.swift index 104cecbf..5c61be3d 100644 --- a/cmuxTests/BrowserPanelTests.swift +++ b/cmuxTests/BrowserPanelTests.swift @@ -13,7 +13,7 @@ import UserNotifications @testable import cmux #endif -func drainMainQueue() { +private func drainBrowserPanelMainQueue() { let expectation = XCTestExpectation(description: "drain main queue") DispatchQueue.main.async { expectation.fulfill() @@ -22,7 +22,7 @@ func drainMainQueue() { } @MainActor -func makeTemporaryBrowserProfile(named prefix: String) throws -> BrowserProfileDefinition { +private func makeTemporaryBrowserPanelProfile(named prefix: String) throws -> BrowserProfileDefinition { try XCTUnwrap( BrowserProfileStore.shared.createProfile( named: "\(prefix)-\(UUID().uuidString)" @@ -107,7 +107,7 @@ final class BrowserPanelOmnibarPillBackgroundColorTests: XCTestCase { @MainActor final class BrowserPanelProfileIsolationTests: XCTestCase { func testStaleDidFinishDoesNotRecordVisitIntoSwitchedProfileHistory() throws { - let alternateProfile = try makeTemporaryBrowserProfile(named: "Switched") + let alternateProfile = try makeTemporaryBrowserPanelProfile(named: "Switched") let defaultStore = BrowserHistoryStore.shared let alternateStore = BrowserProfileStore.shared.historyStore(for: alternateProfile.id) defaultStore.clearHistory() @@ -137,7 +137,7 @@ final class BrowserPanelProfileIsolationTests: XCTestCase { alternateStore.clearHistory() staleDelegate.webView?(staleWebView, didFinish: nil) - drainMainQueue() + drainBrowserPanelMainQueue() XCTAssertTrue( defaultStore.entries.isEmpty, diff --git a/cmuxTests/WindowAndDragTests.swift b/cmuxTests/WindowAndDragTests.swift index e618949f..6964c45f 100644 --- a/cmuxTests/WindowAndDragTests.swift +++ b/cmuxTests/WindowAndDragTests.swift @@ -299,6 +299,42 @@ final class FocusFlashPatternTests: XCTestCase { } +@available(macOS 26.0, *) +private struct DragConfigurationOperationsSnapshot: Equatable { + let allowCopy: Bool + let allowMove: Bool + let allowDelete: Bool + let allowAlias: Bool +} + +@available(macOS 26.0, *) +private enum DragConfigurationSnapshotError: Error { + case missingBoolField(primary: String, fallback: String?) +} + +@available(macOS 26.0, *) +private func dragConfigurationOperationsSnapshot(from operations: T) throws -> DragConfigurationOperationsSnapshot { + let mirror = Mirror(reflecting: operations) + + func readBool(_ primary: String, fallback: String? = nil) throws -> Bool { + if let value = mirror.descendant(primary) as? Bool { + return value + } + if let fallback, let value = mirror.descendant(fallback) as? Bool { + return value + } + throw DragConfigurationSnapshotError.missingBoolField(primary: primary, fallback: fallback) + } + + return try DragConfigurationOperationsSnapshot( + allowCopy: readBool("allowCopy", fallback: "_allowCopy"), + allowMove: readBool("allowMove", fallback: "_allowMove"), + allowDelete: readBool("allowDelete", fallback: "_allowDelete"), + allowAlias: readBool("allowAlias", fallback: "_allowAlias") + ) +} + +#if compiler(>=6.2) @MainActor final class InternalTabDragConfigurationTests: XCTestCase { func testDisablesExternalOperationsForInternalTabDrags() throws { @@ -1080,3 +1116,4 @@ final class MarkdownPanelPointerObserverViewTests: XCTestCase { XCTAssertNil(overlay.hitTest(NSPoint(x: 40, y: 30))) } } +#endif diff --git a/cmuxTests/WorkspaceManualUnreadTests.swift b/cmuxTests/WorkspaceManualUnreadTests.swift index 1610dc34..6fbe30c3 100644 --- a/cmuxTests/WorkspaceManualUnreadTests.swift +++ b/cmuxTests/WorkspaceManualUnreadTests.swift @@ -7,6 +7,7 @@ import AppKit @testable import cmux #endif +@MainActor final class WorkspaceManualUnreadTests: XCTestCase { func testShouldClearManualUnreadWhenFocusMovesToDifferentPanel() { let previousFocusedPanelId = UUID() @@ -281,7 +282,7 @@ final class CommandPaletteSwitcherSearchIndexerTests: XCTestCase { XCTAssertFalse(keywords.contains("cmd-palette-indexing")) } - func testSurfaceDetailOutranksWorkspaceDetailForPathToken() { + func testSurfaceDetailOutranksWorkspaceDetailForPathToken() throws { let metadata = CommandPaletteSwitcherSearchMetadata( directories: ["/tmp/worktrees/cmux"], branches: ["feature/cmd-palette"], diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index 5bd83682..719baa43 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -1896,4 +1896,3 @@ final class SidebarWorkspaceShortcutHintMetricsTests: XCTestCase { XCTAssertGreaterThan(widened, base) } } -#endif