Revert UI test foreground activation changes back to 56a4d258

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 <noreply@anthropic.com>
This commit is contained in:
austinpower1258 2026-03-23 02:38:06 -07:00
parent 82a16aa746
commit fc858fcfa4
3 changed files with 35 additions and 187 deletions

View file

@ -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: |

View file

@ -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)

View file

@ -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"] ?? "<unset>")"
])
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,