From 52c4d9a5a47889eea08f43c8b18bcfff9153a638 Mon Sep 17 00:00:00 2001 From: BHARATH CHANDRA VADDARAM Date: Mon, 19 Oct 2026 22:38:07 -0500 Subject: [PATCH] fix(ci): pass the macOS 14 job (whisper probe root, hidden AI tab) Hosted CI's macOS 14 job has failed on main since 2026-10-08, with two causes. WhisperServerSetup.readinessProbeURL used URL.deletingLastPathComponent(). On macOS 14 Foundation that turns a root path "/" into "/../", so the readiness probe for a spawned whisper-server (http://127.0.0.1:/) left the root it is bound to. That is a real bug, not only a test failure. The parent path is now computed from the percent-encoded path, so every OS gives the same answer. A new test covers the empty, root, double-slash, trailing-slash, percent-encoded and IPv6 shapes. The empty-path case fails on the old code on current macOS too ("http://127.0.0.1:8080" was returned with no path). testOpeningPreferencesFirstTimeReloadsAllExactlyOnce opened Preferences on .ai and counted the AI pane's reloads. Before macOS 26 the AI tab is hidden (PreferencesTab.visibleCases) and select(.ai) presents General, so the AI count was nil. The product behaviour is right and the test assumed the tab was visible. It now checks the tab actually presented, and on older systems also checks that the hidden AI pane is never built. Co-Authored-By: Claude Opus 5.5 --- .../Recognition/WhisperServerSetup.swift | 12 ++++++++++- .../UIPerformanceReuseTests.swift | 7 ++++++- .../WhisperServerSetupTests.swift | 21 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/Sources/ExpanderEngine/Voice/Recognition/WhisperServerSetup.swift b/Sources/ExpanderEngine/Voice/Recognition/WhisperServerSetup.swift index a0b4773..0f74511 100644 --- a/Sources/ExpanderEngine/Voice/Recognition/WhisperServerSetup.swift +++ b/Sources/ExpanderEngine/Voice/Recognition/WhisperServerSetup.swift @@ -126,8 +126,18 @@ public enum WhisperServerSetup { /// The stock server exposes a GET page at its request-path root; it does not provide a /// `/health` route. For the default `/inference` endpoint this is `/`, while a custom /// endpoint such as `/api/inference` is probed at `/api/`. + /// + /// The parent path is computed here rather than with `URL.deletingLastPathComponent()`: + /// on macOS 14 that call turns a root path `/` into `/../`, so the probe for a spawned + /// child (`http://127.0.0.1:/`) left the root it is bound to. static func readinessProbeURL(for endpoint: URL) -> URL { - endpoint.deletingLastPathComponent() + guard var components = URLComponents(url: endpoint, resolvingAgainstBaseURL: false) else { + return endpoint + } + var segments = components.percentEncodedPath.split(separator: "/") + if !segments.isEmpty { segments.removeLast() } + components.percentEncodedPath = segments.isEmpty ? "/" : "/" + segments.joined(separator: "/") + "/" + return components.url ?? endpoint } /// Readiness URL for a `whisper-server` this controller has just spawned. diff --git a/Tests/DevTypeAppTests/UIPerformanceReuseTests.swift b/Tests/DevTypeAppTests/UIPerformanceReuseTests.swift index d405fcb..b796f19 100644 --- a/Tests/DevTypeAppTests/UIPerformanceReuseTests.swift +++ b/Tests/DevTypeAppTests/UIPerformanceReuseTests.swift @@ -50,7 +50,12 @@ final class UIPerformanceReuseTests: XCTestCase { XCTAssertEqual(prefs.reloadAllCount, 1, "viewDidLoad, viewWillAppear and show() each reloaded") // The tab selected on open is populated by being built, then left alone by the pass. - XCTAssertEqual(prefs.paneReloadCounts[.ai], 1, "first AI presentation reloaded twice") + // Before macOS 26 the AI tab is hidden and `select(.ai)` presents General instead. + let presented: PreferencesTab = PreferencesTab.visibleCases.contains(.ai) ? .ai : .general + XCTAssertEqual(prefs.paneReloadCounts[presented], 1, "first \(presented) presentation reloaded twice") + if presented != .ai { + XCTAssertNil(prefs.paneReloadCounts[.ai], "a hidden AI tab must not be built or read") + } } func testFirstAdvancedPresentationDoesNotReloadTwice() throws { diff --git a/Tests/ExpanderEngineTests/WhisperServerSetupTests.swift b/Tests/ExpanderEngineTests/WhisperServerSetupTests.swift index d3330ad..6b34c3e 100644 --- a/Tests/ExpanderEngineTests/WhisperServerSetupTests.swift +++ b/Tests/ExpanderEngineTests/WhisperServerSetupTests.swift @@ -178,6 +178,27 @@ final class WhisperServerSetupTests: XCTestCase { ) } + /// The probe path is computed, not left to `deletingLastPathComponent`, whose answer for a + /// root or empty path differs by OS (macOS 14 turns `/` into `/../`). Every shape lands + /// on an explicit request-path root and keeps host, port and percent-encoding. + func testReadinessProbeIsAnExplicitRootForEveryPathShape() { + let cases: [(String, String)] = [ + ("http://127.0.0.1:8080", "http://127.0.0.1:8080/"), + ("http://127.0.0.1:8080/", "http://127.0.0.1:8080/"), + ("http://127.0.0.1:8080//", "http://127.0.0.1:8080/"), + ("http://127.0.0.1:8080/api/inference/", "http://127.0.0.1:8080/api/"), + ("http://127.0.0.1:8080/a%20b/inference", "http://127.0.0.1:8080/a%20b/"), + ("http://[::1]:9090/inference", "http://[::1]:9090/") + ] + for (endpoint, expected) in cases { + XCTAssertEqual( + WhisperServerSetup.readinessProbeURL(for: URL(string: endpoint)!).absoluteString, + expected, + endpoint + ) + } + } + /// The spawned child is bound to 127.0.0.1 even when the saved endpoint says localhost or ::1. func testSpawnedChildProbeURLUsesTheBoundLoopbackAddress() { XCTAssertEqual(