From 386f5abf67247b853bdbbbbe8d85f81969e199d1 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sat, 28 Mar 2026 14:17:16 -0700 Subject: [PATCH] Fix macOS compatibility: versioned geometry persistence and re-entrant layout crash (#2308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- cmuxTests/TerminalAndGhosttyTests.swift | 44 ++++++++++++++++--- ...erminalControllerSocketSecurityTests.swift | 2 +- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/cmuxTests/TerminalAndGhosttyTests.swift b/cmuxTests/TerminalAndGhosttyTests.swift index 7bbdddc6..5ff4a9cc 100644 --- a/cmuxTests/TerminalAndGhosttyTests.swift +++ b/cmuxTests/TerminalAndGhosttyTests.swift @@ -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") } diff --git a/cmuxTests/TerminalControllerSocketSecurityTests.swift b/cmuxTests/TerminalControllerSocketSecurityTests.swift index 35a0ff70..8f4232ce 100644 --- a/cmuxTests/TerminalControllerSocketSecurityTests.swift +++ b/cmuxTests/TerminalControllerSocketSecurityTests.swift @@ -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)