cmux/cmuxUITests/AutomationSocketUITests.swift
2026-03-16 23:57:48 -07:00

142 lines
4.9 KiB
Swift

import XCTest
import Foundation
final class AutomationSocketUITests: XCTestCase {
private var socketPath = ""
private let defaultsDomain = "com.cmuxterm.app.debug"
private let modeKey = "socketControlMode"
private let legacyKey = "socketControlEnabled"
private let launchTag = "ui-tests-automation-socket"
override func setUp() {
super.setUp()
continueAfterFailure = false
socketPath = "/tmp/cmux-debug-\(UUID().uuidString).sock"
resetSocketDefaults()
removeSocketFile()
}
func testSocketToggleDisablesAndEnables() {
let app = configuredApp(mode: "cmuxOnly")
app.launch()
XCTAssertTrue(
ensureForegroundAfterLaunch(app, timeout: 12.0),
"Expected app to launch for socket toggle test. state=\(app.state.rawValue)"
)
guard let resolvedPath = resolveSocketPath(timeout: 5.0) else {
XCTFail("Expected control socket to exist")
return
}
socketPath = resolvedPath
XCTAssertTrue(waitForSocket(exists: true, timeout: 2.0))
app.terminate()
}
func testSocketDisabledWhenSettingOff() {
let app = configuredApp(mode: "off")
app.launch()
XCTAssertTrue(
ensureForegroundAfterLaunch(app, timeout: 12.0),
"Expected app to launch for socket off test. state=\(app.state.rawValue)"
)
XCTAssertTrue(waitForSocket(exists: false, timeout: 3.0))
app.terminate()
}
private func configuredApp(mode: String) -> XCUIApplication {
let app = XCUIApplication()
app.launchArguments += ["-\(modeKey)", mode]
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
app.launchEnvironment["CMUX_UI_TEST_SOCKET_SANITY"] = "1"
// Debug launches require a tag outside reload.sh; provide one in UITests so CI
// does not fail with "Application ... does not have a process ID".
app.launchEnvironment["CMUX_TAG"] = launchTag
return app
}
private func ensureForegroundAfterLaunch(_ app: XCUIApplication, timeout: TimeInterval) -> Bool {
if app.wait(for: .runningForeground, timeout: timeout) {
return true
}
// On busy UI runners the app can launch backgrounded; activate once before failing.
if app.state == .runningBackground {
app.activate()
return app.wait(for: .runningForeground, timeout: 6.0)
}
return false
}
private func waitForSocket(exists: Bool, timeout: TimeInterval) -> Bool {
let expectation = XCTNSPredicateExpectation(
predicate: NSPredicate { _, _ in
FileManager.default.fileExists(atPath: self.socketPath) == exists
},
object: NSObject()
)
return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed
}
private func resolveSocketPath(timeout: TimeInterval) -> String? {
var resolvedPath: String?
let expectation = XCTNSPredicateExpectation(
predicate: NSPredicate { _, _ in
if FileManager.default.fileExists(atPath: self.socketPath) {
resolvedPath = self.socketPath
return true
}
if let found = self.findSocketInTmp() {
resolvedPath = found
return true
}
return false
},
object: NSObject()
)
if XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed {
return resolvedPath
}
return resolvedPath
}
private func findSocketInTmp() -> String? {
let tmpPath = "/tmp"
guard let entries = try? FileManager.default.contentsOfDirectory(atPath: tmpPath) else {
return nil
}
let matches = entries.filter { $0.hasPrefix("cmux") && $0.hasSuffix(".sock") }
if let debug = matches.first(where: { $0.contains("debug") }) {
return (tmpPath as NSString).appendingPathComponent(debug)
}
if let first = matches.first {
return (tmpPath as NSString).appendingPathComponent(first)
}
return nil
}
private func resetSocketDefaults() {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/defaults")
process.arguments = ["delete", defaultsDomain, modeKey]
do {
try process.run()
process.waitUntilExit()
} catch {
return
}
let legacy = Process()
legacy.executableURL = URL(fileURLWithPath: "/usr/bin/defaults")
legacy.arguments = ["delete", defaultsDomain, legacyKey]
do {
try legacy.run()
legacy.waitUntilExit()
} catch {
return
}
}
private func removeSocketFile() {
try? FileManager.default.removeItem(atPath: socketPath)
}
}