Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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:<port>/`) 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.
Expand Down
7 changes: 6 additions & 1 deletion Tests/DevTypeAppTests/UIPerformanceReuseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
21 changes: 21 additions & 0 deletions Tests/ExpanderEngineTests/WhisperServerSetupTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading