From fc858fcfa495c60ba6a6b0a14ac08c441b45e1bf Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 23 Mar 2026 02:38:06 -0700 Subject: [PATCH] Revert UI test foreground activation changes back to 56a4d258 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts cbb21872, 54ec524a, 10fd323b, 75375ab7, 82a16aa7 — all attempts to fix display resolution UI test foreground activation on CI that introduced regressions. Restores the state from the last fully green CI run (56a4d258). Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 24 +--- Sources/AppDelegate.swift | 76 +---------- .../DisplayResolutionRegressionUITests.swift | 122 ++++-------------- 3 files changed, 35 insertions(+), 187 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1aa05eb..5cf24332 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -495,24 +495,12 @@ jobs: {"helperBinaryPath":"$HELPER_PATH"} EOF - # Retry once — foreground activation on headless CI runners is flaky - for attempt in 1 2; do - if xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \ - -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ - -disableAutomaticPackageResolution \ - -destination "platform=macOS" \ - -only-testing:cmuxUITests/DisplayResolutionRegressionUITests \ - test; then - exit 0 - fi - if [ "$attempt" -eq 2 ]; then - echo "Display resolution UI regression failed after 2 attempts" >&2 - exit 1 - fi - echo "Attempt $attempt failed, retrying..." - pkill -x "cmux DEV" 2>/dev/null || true - sleep 3 - done + xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \ + -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ + -disableAutomaticPackageResolution \ + -destination "platform=macOS" \ + -only-testing:cmuxUITests/DisplayResolutionRegressionUITests \ + test - name: Run browser find focus UI regression run: | diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 54b36d56..0aa385ef 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -1929,10 +1929,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent Self.cachedIsRunningUnderXCTest } -#if DEBUG - private var uiTestForegroundActivationInFlight = false -#endif - private static func detectRunningUnderXCTest(_ env: [String: String]) -> Bool { if env["XCTestConfigurationFilePath"] != nil { return true } if env["XCTestBundlePath"] != nil { return true } @@ -2369,7 +2365,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent // In UI tests, `WindowGroup` occasionally fails to materialize a window quickly on the VM. // If there are no windows shortly after launch, force-create one so XCUITest can proceed. if isRunningUnderXCTest { - scheduleUITestForegroundActivationIfNeeded(reason: "didFinishLaunching") if let rawVariant = env["CMUX_UI_TEST_BROWSER_IMPORT_HINT_VARIANT"] { UserDefaults.standard.set( BrowserImportHintSettings.variant(for: rawVariant).rawValue, @@ -2394,7 +2389,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent self.openNewMainWindow(nil) } self.moveUITestWindowToTargetDisplayIfNeeded() - self.scheduleUITestForegroundActivationIfNeeded(reason: "afterForceWindow") + NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps]) self.writeUITestDiagnosticsIfNeeded(stage: "afterForceWindow") } if env["CMUX_UI_TEST_BROWSER_IMPORT_HINT_OPEN_BLANK_BROWSER"] == "1" { @@ -2420,12 +2415,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent #endif } - func applicationWillFinishLaunching(_ notification: Notification) { -#if DEBUG - scheduleUITestForegroundActivationIfNeeded(reason: "willFinishLaunching") -#endif - } - #if DEBUG private func writeUITestDiagnosticsIfNeeded(stage: String) { let env = ProcessInfo.processInfo.environment @@ -2444,8 +2433,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent payload["pid"] = String(ProcessInfo.processInfo.processIdentifier) payload["bundleId"] = Bundle.main.bundleIdentifier ?? "" payload["isRunningUnderXCTest"] = isRunningUnderXCTest ? "1" : "0" - payload["appIsActive"] = NSApp.isActive ? "1" : "0" - payload["activationPolicy"] = debugActivationPolicyDescription(NSApp.activationPolicy()) payload["windowsCount"] = String(windows.count) payload["windowIdentifiers"] = ids payload["windowVisibleFlags"] = vis @@ -2577,66 +2564,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent ) } - private func scheduleUITestForegroundActivationIfNeeded(reason: String) { - let env = ProcessInfo.processInfo.environment - guard isRunningUnderXCTest(env) else { return } - guard !uiTestForegroundActivationInFlight else { return } - - uiTestForegroundActivationInFlight = true - attemptUITestForegroundActivation(reason: reason, attempt: 0) - } - - private func attemptUITestForegroundActivation(reason: String, attempt: Int) { - let env = ProcessInfo.processInfo.environment - guard isRunningUnderXCTest(env) else { - uiTestForegroundActivationInFlight = false - return - } - - if NSApp.activationPolicy() != .regular { - _ = NSApp.setActivationPolicy(.regular) - } - if NSApp.isHidden { - NSApp.unhide(nil) - } - - let windows = NSApp.windows - if let window = windows.first { - window.orderFrontRegardless() - window.makeKeyAndOrderFront(nil) - } - - NSApp.activate(ignoringOtherApps: true) - NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps]) - writeUITestDiagnosticsIfNeeded(stage: "foreground.\(reason).\(attempt)") - - if NSApp.isActive { - uiTestForegroundActivationInFlight = false - return - } - guard attempt < 20 else { - uiTestForegroundActivationInFlight = false - return - } - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in - self?.attemptUITestForegroundActivation(reason: reason, attempt: attempt + 1) - } - } - - private func debugActivationPolicyDescription(_ policy: NSApplication.ActivationPolicy) -> String { - switch policy { - case .regular: - return "regular" - case .accessory: - return "accessory" - case .prohibited: - return "prohibited" - @unknown default: - return "unknown(\(policy.rawValue))" - } - } - private func moveUITestWindowToTargetDisplayIfNeeded(attempt: Int = 0) { let env = ProcessInfo.processInfo.environment guard let rawDisplayID = env["CMUX_UI_TEST_TARGET_DISPLAY_ID"], @@ -2677,7 +2604,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent window.setFrame(frame, display: true, animate: false) window.makeKeyAndOrderFront(nil) window.orderFrontRegardless() - scheduleUITestForegroundActivationIfNeeded(reason: "afterMoveToTargetDisplay") if window.screen?.cmuxDisplayID != targetDisplayID, attempt < 20 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in self?.moveUITestWindowToTargetDisplayIfNeeded(attempt: attempt + 1) diff --git a/cmuxUITests/DisplayResolutionRegressionUITests.swift b/cmuxUITests/DisplayResolutionRegressionUITests.swift index e591afee..1cc5aecf 100644 --- a/cmuxUITests/DisplayResolutionRegressionUITests.swift +++ b/cmuxUITests/DisplayResolutionRegressionUITests.swift @@ -1,10 +1,8 @@ import XCTest import Foundation -import AppKit final class DisplayResolutionRegressionUITests: XCTestCase { private let defaultDisplayHarnessManifestPath = "/tmp/cmux-ui-test-display-harness.json" - private let appBundleIdentifier = "com.cmuxterm.app.debug" private var launchTag = "" private var diagnosticsPath = "" private var displayReadyPath = "" @@ -13,7 +11,7 @@ final class DisplayResolutionRegressionUITests: XCTestCase { private var displayDonePath = "" private var helperBinaryPath = "" private var helperLogPath = "" - private var launchedRunningApp: NSRunningApplication? + private var launchedApp: XCUIApplication? private var helperProcess: Process? override func setUp() { @@ -241,79 +239,18 @@ final class DisplayResolutionRegressionUITests: XCTestCase { helperProcess = proc } - // Launch the app via NSWorkspace instead of XCUIApplication to avoid - // the 60-second foreground activation timeout that kills UI tests on - // headless CI runners. NSWorkspace.openApplication passes environment - // variables through OpenConfiguration and returns immediately. private func launchAppProcess(targetDisplayID: String) throws { - let appBundlePath = try resolveAppBundlePath() - let appURL = URL(fileURLWithPath: appBundlePath) - - let config = NSWorkspace.OpenConfiguration() - config.environment = launchEnvironment(targetDisplayID: targetDisplayID) - config.activates = true - - let semaphore = DispatchSemaphore(value: 0) - var launchError: Error? - var runningApp: NSRunningApplication? - - NSWorkspace.shared.openApplication(at: appURL, configuration: config) { app, error in - runningApp = app - launchError = error - semaphore.signal() + let app = XCUIApplication() + for (key, value) in launchEnvironment(targetDisplayID: targetDisplayID) { + app.launchEnvironment[key] = value } - - let waitResult = semaphore.wait(timeout: .now() + 30.0) - if waitResult == .timedOut { + app.launch() + guard ensureForegroundAfterLaunch(app, timeout: 12.0) else { throw NSError(domain: "DisplayResolutionRegressionUITests", code: 2, userInfo: [ - NSLocalizedDescriptionKey: "NSWorkspace.openApplication timed out after 30s for \(appBundlePath)" + NSLocalizedDescriptionKey: "XCUIApplication failed to reach foreground. state=\(app.state.rawValue)" ]) } - - if let error = launchError { - throw NSError(domain: "DisplayResolutionRegressionUITests", code: 2, userInfo: [ - NSLocalizedDescriptionKey: "NSWorkspace.openApplication failed: \(error.localizedDescription) path=\(appBundlePath)" - ]) - } - - launchedRunningApp = runningApp - - if !waitForAppLaunchDiagnostics(timeout: 15.0) { - let isAlive = launchedRunningApp?.isTerminated == false - throw NSError(domain: "DisplayResolutionRegressionUITests", code: 2, userInfo: [ - NSLocalizedDescriptionKey: "App failed to write launch diagnostics. alive=\(isAlive) diagnostics=\(loadDiagnostics() ?? [:])" - ]) - } - } - - private func resolveAppBundlePath() throws -> String { - let testBundle = Bundle(for: Self.self) - // UI test bundle is at: - // .../Build/Products/Debug/cmuxUITests-Runner.app/Contents/PlugIns/cmuxUITests.xctest - // The app is at: - // .../Build/Products/Debug/cmux DEV.app - let productsDir = testBundle.bundleURL - .deletingLastPathComponent() // PlugIns/ - .deletingLastPathComponent() // Contents/ - .deletingLastPathComponent() // cmuxUITests-Runner.app/ - let appPath = productsDir.appendingPathComponent("cmux DEV.app").path - if FileManager.default.fileExists(atPath: appPath) { - return appPath - } - - // Fallback: search DerivedData for the app - let env = ProcessInfo.processInfo.environment - if let builtProductsDir = env["BUILT_PRODUCTS_DIR"], !builtProductsDir.isEmpty { - let candidate = URL(fileURLWithPath: builtProductsDir) - .appendingPathComponent("cmux DEV.app").path - if FileManager.default.fileExists(atPath: candidate) { - return candidate - } - } - - throw NSError(domain: "DisplayResolutionRegressionUITests", code: 4, userInfo: [ - NSLocalizedDescriptionKey: "App bundle not found. primary=\(appPath) BUILT_PRODUCTS_DIR=\(env["BUILT_PRODUCTS_DIR"] ?? "")" - ]) + launchedApp = app } private func launchEnvironment(targetDisplayID: String) -> [String: String] { @@ -327,25 +264,31 @@ final class DisplayResolutionRegressionUITests: XCTestCase { } private func terminateLaunchedAppIfNeeded() { - guard let app = launchedRunningApp else { return } - defer { launchedRunningApp = nil } + guard let launchedApp else { return } + defer { self.launchedApp = nil } - if app.isTerminated { return } + if launchedApp.state == .notRunning { + return + } - app.terminate() - // Wait up to 5s for termination - let deadline = Date().addingTimeInterval(5.0) - while !app.isTerminated && Date() < deadline { - RunLoop.current.run(until: Date().addingTimeInterval(0.1)) - } - if !app.isTerminated { - app.forceTerminate() - } + launchedApp.terminate() + _ = launchedApp.wait(for: .notRunning, timeout: 5.0) } private func launchedAppDiagnostics() -> String { - guard let app = launchedRunningApp else { return "not-launched" } - return "pid=\(app.processIdentifier) terminated=\(app.isTerminated)" + guard let launchedApp else { return "not-launched" } + return "state=\(launchedApp.state.rawValue)" + } + + private func ensureForegroundAfterLaunch(_ app: XCUIApplication, timeout: TimeInterval) -> Bool { + if app.wait(for: .runningForeground, timeout: timeout) { + return true + } + if app.state == .runningBackground { + app.activate() + return app.wait(for: .runningForeground, timeout: 6.0) + } + return false } private func waitForTargetDisplayMove(targetDisplayID: String, timeout: TimeInterval) -> Bool { @@ -412,15 +355,6 @@ final class DisplayResolutionRegressionUITests: XCTestCase { .deletingLastPathComponent() } - private func waitForAppLaunchDiagnostics(timeout: TimeInterval) -> Bool { - waitForCondition(timeout: timeout) { - guard let diagnostics = self.loadDiagnostics() else { return false } - guard let pid = diagnostics["pid"], !pid.isEmpty else { return false } - guard let stage = diagnostics["stage"], !stage.isEmpty else { return false } - return true - } - } - private func removeTestArtifacts() { for path in [ diagnosticsPath,