Fix macOS compatibility: versioned geometry persistence and re-entrant layout crash (#2308)

- Version the persisted window geometry schema (v1 → v2) and clean up
  legacy UserDefaults keys so stale payloads from older releases don't
  cause crashes on startup.
- Defer layout follow-up flush via asyncAfter(0) and track an attempt
  version counter to invalidate stale retries, preventing re-entrant
  displayIfNeeded crashes triggered by SwiftUI geometry change callbacks.
- Replace fixed RunLoop delays in tests with polling waitUntil helpers
  and increase socket wait timeout for CI reliability.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Austin Wang 2026-03-28 14:17:16 -07:00 committed by GitHub
parent f1be3978ab
commit 386f5abf67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 40 additions and 6 deletions

View file

@ -1731,6 +1731,31 @@ final class GhosttySurfaceOverlayTests: XCTestCase {
return false
}
@discardableResult
private func waitUntil(
timeout: TimeInterval = 1.0,
description: String,
file: StaticString = #filePath,
line: UInt = #line,
_ condition: @escaping () -> Bool
) -> Bool {
let expectation = XCTNSPredicateExpectation(
predicate: NSPredicate { _, _ in
if Thread.isMainThread {
return condition()
}
return DispatchQueue.main.sync(execute: condition)
},
object: NSObject()
)
let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
guard result == .completed else {
XCTFail("Timed out waiting for \(description)", file: file, line: line)
return false
}
return true
}
func testTrackpadScrollRoutesToTerminalSurfaceAndPreservesKeyboardFocusPath() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 360, height: 240),
@ -2045,11 +2070,15 @@ final class GhosttySurfaceOverlayTests: XCTestCase {
let searchState = TerminalSurface.SearchState(needle: "example")
hostedView.setSearchOverlay(searchState: searchState)
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
waitUntil(description: "search overlay to mount") {
hostedView.debugHasSearchOverlay()
}
XCTAssertTrue(hostedView.debugHasSearchOverlay())
hostedView.setSearchOverlay(searchState: nil)
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
waitUntil(description: "search overlay to unmount") {
!hostedView.debugHasSearchOverlay()
}
XCTAssertFalse(hostedView.debugHasSearchOverlay())
}
@ -2342,12 +2371,17 @@ final class GhosttySurfaceOverlayTests: XCTestCase {
)
weakSurface = surface
let hostedView = surface.hostedView
hostedView.setSearchOverlay(searchState: TerminalSurface.SearchState(needle: "retain-check"))
return hostedView
hostedView.setSearchOverlay(searchState: TerminalSurface.SearchState(needle: "retain-check"))
return hostedView
}()
RunLoop.main.run(until: Date().addingTimeInterval(0.01))
waitUntil(description: "search overlay to mount") {
hostedView.debugHasSearchOverlay()
}
XCTAssertTrue(hostedView.debugHasSearchOverlay())
waitUntil(description: "terminal surface to deallocate after search overlay mount") {
weakSurface == nil
}
XCTAssertNil(weakSurface, "Mounted search overlay must not retain TerminalSurface")
}

View file

@ -254,7 +254,7 @@ final class TerminalControllerSocketSecurityTests: XCTestCase {
XCTAssertTrue(manager.tabs.contains(where: { $0.id == pinnedWorkspace.id }))
}
private func waitForSocket(at path: String, timeout: TimeInterval = 2.0) throws {
private func waitForSocket(at path: String, timeout: TimeInterval = 5.0) throws {
let expectation = XCTNSPredicateExpectation(
predicate: NSPredicate { _, _ in
FileManager.default.fileExists(atPath: path)