diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 558226ae5dde..e4c26976f1c3 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -355,6 +355,19 @@ class GhosttyApp { private(set) var app: ghostty_app_t? private(set) var config: ghostty_config_t? +#if DEBUG + /// Installs `newConfig` as the app config and returns the previous one, + /// which the caller then owns. Tests change a setting on a clone through + /// this instead of re-loading into the live config, which is finalized. + func swapConfigForTesting(_ newConfig: ghostty_config_t) -> ghostty_config_t? { + if let app { + ghostty_app_update_config_without_surface_propagation(app, newConfig) + } + let previous = config + config = newConfig + return previous + } +#endif /// Coalesce wakeup → tick dispatches. The I/O thread may fire wakeup_cb /// thousands of times per second during bulk output. We only need one /// pending tick on the main queue at any time. diff --git a/cmuxTests/CJKIMEInputTests+DeadKeyComposition.swift b/cmuxTests/CJKIMEInputTests+DeadKeyComposition.swift index bf6244cca326..12ebb62e3640 100644 --- a/cmuxTests/CJKIMEInputTests+DeadKeyComposition.swift +++ b/cmuxTests/CJKIMEInputTests+DeadKeyComposition.swift @@ -118,34 +118,36 @@ extension DeadKeyCompositionRegressionTests { XCTAssertEqual(pressedKeycodes, [], "Dead-key handling must not leak raw key events") } + /// Installs a clone of the live Ghostty config with `macos-option-as-alt` + /// set to `value` (or unset for nil) and returns a closure that puts the + /// original config back. Loading into the live config directly fails: it + /// is already finalized, and the load ends in a crash. func installOptionAsAltConfiguration(_ value: String?) -> () -> Void { - guard let app = GhosttyApp.shared.app, - let config = GhosttyApp.shared.config else { + guard let base = GhosttyApp.shared.config, + let clone = ghostty_config_clone(base) else { XCTFail("Expected Ghostty app configuration") return {} } let key = "macos-option-as-alt" - let keyLength = UInt(key.utf8.count) - var originalValue: UnsafePointer? - let hadOriginalValue = ghostty_config_get(config, &originalValue, key, keyLength) - let original = hadOriginalValue ? originalValue.map { String(cString: $0) } : nil - - func apply(_ setting: String?) { - let contents = setting.map { "\(key) = \($0)\n" } ?? "\(key) =\n" - contents.withCString { pointer in - ghostty_config_load_string( - config, - pointer, - UInt(contents.utf8.count), - "/__cmux_test__/option-as-alt.conf" - ) + let contents = value.map { "\(key) = \($0)\n" } ?? "\(key) =\n" + contents.withCString { pointer in + ghostty_config_load_string( + clone, + pointer, + UInt(contents.utf8.count), + "/__cmux_test__/option-as-alt.conf" + ) + } + ghostty_config_finalize(clone) + guard let original = GhosttyApp.shared.swapConfigForTesting(clone) else { + XCTFail("Expected Ghostty app configuration") + return {} + } + return { + if let installed = GhosttyApp.shared.swapConfigForTesting(original) { + ghostty_config_free(installed) } - ghostty_config_finalize(config) - ghostty_app_update_config_without_surface_propagation(app, config) } - - apply(value) - return { apply(original) } } } diff --git a/cmuxTests/SidebarAccessibilityTreeTests.swift b/cmuxTests/SidebarAccessibilityTreeTests.swift index 6026919c3f00..29516150b1f7 100644 --- a/cmuxTests/SidebarAccessibilityTreeTests.swift +++ b/cmuxTests/SidebarAccessibilityTreeTests.swift @@ -95,8 +95,10 @@ struct SidebarAccessibilityTreeTests { #expect(walk.maxDepth < 256, "Accessibility walk exceeded the safety depth: \(walk.maxDepth)") #expect(walk.visited.contains(ObjectIdentifier(textView))) #expect(walk.visited.contains(ObjectIdentifier(link))) - // NSHostingView can be ignored in the AX tree; verify its rendered content. - #expect(walk.textValues.contains { $0.contains("Context.swift") }) + // The walk still descends into the project panel's NSHostingView, so the + // cycle and depth checks cover it. Its SwiftUI rows are not asserted: + // with no assistive client attached, SwiftUI does not vend them in the + // app host, and the walk only ever saw the sidebar row's text. let updated = SidebarWorkspaceRowSuspensionTests.makeModel( customDescription: "Changed https://example.com/updated", workspaceId: model.workspaceId