Run notify regression from in-app shell

This commit is contained in:
Lawrence Chen 2026-03-05 20:04:52 -08:00
parent f9dbbd3562
commit 6c163b1eb0
2 changed files with 104 additions and 19 deletions

View file

@ -5750,6 +5750,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
"expectedLatestWindowId": window1.windowId.uuidString,
"expectedLatestTabId": tabId1.uuidString,
], at: path)
// Leave the initial window's terminal focused so UI tests can type shell
// commands while still keeping the second window configured for notifications.
window1.window?.makeKeyAndOrderFront(nil)
self.publishMultiWindowNotificationSocketStateIfNeeded(at: path)
}
}

View file

@ -248,40 +248,83 @@ final class MultiWindowNotificationsUITests: XCTestCase {
XCTAssertTrue(waitForWindowCount(atLeast: 2, app: app, timeout: 6.0))
let title = "focus-regression-\(UUID().uuidString.prefix(8))"
let commandResultStem = UUID().uuidString
let commandStatusPath = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-ui-test-notify-\(commandResultStem).status")
.path
let commandStdoutPath = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-ui-test-notify-\(commandResultStem).stdout")
.path
let commandStderrPath = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-ui-test-notify-\(commandResultStem).stderr")
.path
defer {
try? FileManager.default.removeItem(atPath: commandStatusPath)
try? FileManager.default.removeItem(atPath: commandStdoutPath)
try? FileManager.default.removeItem(atPath: commandStderrPath)
}
guard let bundledCLIPath = resolveCmuxCLIPaths(strategy: .bundledOnly).first else {
XCTFail("Failed to locate bundled cmux CLI for notify regression test")
return
}
XCTAssertTrue(app.windows.element(boundBy: 0).waitForExistence(timeout: 4.0), "Expected at least one window before typing notify command")
app.windows.element(boundBy: 0)
.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
.click()
let notifyCommand = [
"rm -f \(shellSingleQuote(commandStatusPath)) \(shellSingleQuote(commandStdoutPath)) \(shellSingleQuote(commandStderrPath));",
"(sleep 1;",
"\(shellSingleQuote(bundledCLIPath))",
"--socket \(shellSingleQuote(socketPath))",
"notify",
"--workspace \(shellSingleQuote(tabId2))",
"--surface \(shellSingleQuote(surfaceId))",
"--title \(shellSingleQuote(title))",
"--subtitle \(shellSingleQuote("ui-test"))",
"--body \(shellSingleQuote("focus-regression"))",
">\(shellSingleQuote(commandStdoutPath))",
"2>\(shellSingleQuote(commandStderrPath));",
"printf '%s' $? >\(shellSingleQuote(commandStatusPath))) >/dev/null 2>&1 &"
].joined(separator: " ")
app.typeText(notifyCommand + "\n")
let finder = XCUIApplication(bundleIdentifier: "com.apple.finder")
finder.activate()
XCTAssertTrue(
waitForAppToLeaveForeground(app, timeout: 8.0),
"Expected cmux to move to background before sending notify command. state=\(app.state.rawValue)"
"Expected cmux to move to background before delayed notify command runs. state=\(app.state.rawValue)"
)
let title = "focus-regression-\(UUID().uuidString.prefix(8))"
let notifyResult = runCmuxNotify(
socketPath: socketPath,
workspaceId: tabId2,
surfaceId: surfaceId,
title: title
XCTAssertTrue(
waitForCommandCompletionWhileBackgrounded(
statusPath: commandStatusPath,
app: app,
timeout: 15.0
),
"Expected delayed bundled `cmux notify` command to finish without foregrounding cmux. state=\(app.state.rawValue)"
)
let notifyExitStatus = readTrimmedFile(atPath: commandStatusPath) ?? "<missing>"
let notifyStdout = readTrimmedFile(atPath: commandStdoutPath) ?? ""
let notifyStderr = readTrimmedFile(atPath: commandStderrPath) ?? ""
RunLoop.current.run(until: Date().addingTimeInterval(0.5))
XCTAssertFalse(
app.state == .runningForeground,
"Expected cmux to remain in background after bundled `cmux notify`. state=\(app.state.rawValue) stderr=\(notifyResult.stderr)"
"Expected cmux to remain in background after bundled `cmux notify`. state=\(app.state.rawValue) stderr=\(notifyStderr)"
)
guard notifyResult.terminationStatus == 0 else {
let rawFallbackResponse: String?
if isSocketPermissionFailure(notifyResult.stderr) {
rawFallbackResponse = socketCommand("notify_target \(tabId2) \(surfaceId) \(title)|ui-test|focus-regression")
} else {
rawFallbackResponse = nil
}
guard notifyExitStatus == "0" else {
XCTFail(
"Expected bundled `cmux notify` to succeed. stderr=\(notifyResult.stderr) " +
"rawFallback=\(rawFallbackResponse ?? "<nil>")"
"Expected bundled `cmux notify` launched from the in-app shell to succeed. " +
"status=\(notifyExitStatus) stdout=\(notifyStdout) stderr=\(notifyStderr)"
)
return
}
XCTAssertTrue(notifyResult.stdout.contains("OK"), "Expected notify command to return OK. stdout=\(notifyResult.stdout)")
XCTAssertTrue(notifyStdout.contains("OK"), "Expected notify command to return OK. stdout=\(notifyStdout) stderr=\(notifyStderr)")
}
private func clickNotificationPopoverRowAndWaitForFocusChange(
@ -437,6 +480,37 @@ final class MultiWindowNotificationsUITests: XCTestCase {
return (stdout ?? lastStdout, stderr ?? lastStderr)
}
private func waitForCommandCompletionWhileBackgrounded(
statusPath: String,
app: XCUIApplication,
timeout: TimeInterval
) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
var sawCompletion = false
while Date() < deadline {
if app.state == .runningForeground {
return false
}
if FileManager.default.fileExists(atPath: statusPath) {
sawCompletion = true
break
}
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
guard sawCompletion || FileManager.default.fileExists(atPath: statusPath) else {
return false
}
let postCompletionDeadline = Date().addingTimeInterval(0.75)
while Date() < postCompletionDeadline {
if app.state == .runningForeground {
return false
}
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
return app.state != .runningForeground
}
private func waitForAppToLeaveForeground(_ app: XCUIApplication, timeout: TimeInterval) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
@ -946,6 +1020,14 @@ final class MultiWindowNotificationsUITests: XCTestCase {
return "'" + value.replacingOccurrences(of: "'", with: "'\"'\"'") + "'"
}
private func readTrimmedFile(atPath path: String) -> String? {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)),
let value = String(data: data, encoding: .utf8) else {
return nil
}
return value.trimmingCharacters(in: .whitespacesAndNewlines)
}
private final class ControlSocketClient {
private let path: String