fix(macos): surface unsaved gateway settings during onboarding (#129959)

This commit is contained in:
Peter Steinberger
2026-08-26 00:41:12 -07:00
committed by GitHub
parent 144e4d5aeb
commit d2360e4a1c
3 changed files with 52 additions and 17 deletions
@@ -13,7 +13,9 @@ extension OnboardingAISetupModel {
return issue
}
func showConfiguredGatewayProbeUnavailable() {
func showConfiguredGatewayProbeUnavailable(
summary: String = "The Gateway did not answer the inference check. Nothing was changed.")
{
guard !self.ownsInferenceTransition ||
self.configuredGatewayBlocker != nil ||
self.waitingForPendingActivationDeadline
@@ -24,9 +26,7 @@ extension OnboardingAISetupModel {
self.updateConfiguredGatewayBlockerState(
.unavailable,
phase: .ready,
detectError: Failure(
summary: "The Gateway did not answer the inference check. Nothing was changed.",
detail: nil))
detectError: Failure(summary: summary, detail: nil))
}
func showConfiguredGatewayAuthIssue(_ issue: RemoteGatewayAuthIssue) {
@@ -155,7 +155,11 @@ extension OnboardingView {
guard !configuredGatewayProbe.isSuppressedForTemporaryConnectionCheck else { return nil }
// Persist the latest selection before GatewayEndpointStore resolves the
// route, so an immediate probe cannot attach to the previous endpoint.
guard gatewaySelectionPersister() else { return nil }
guard gatewaySelectionPersister() else {
self.aiSetup.showConfiguredGatewayProbeUnavailable(
summary: "Could not save Gateway settings. Check your connection settings and try again.")
return nil
}
let expectedMode = state.connectionMode
let expectedRouteIdentity = self.aiSetupRouteIdentityProvider()
let expectedPendingState = OnboardingSystemAgentResumeStore.pendingState(
@@ -750,7 +750,8 @@ private func rejectedSetupVerificationResponse(id: String) -> Data {
private func unconfiguredSetupVerificationResponse(id: String) -> Data {
Data(
#"{"type":"res","id":"\#(id)","ok":true,"payload":{"ok":false,"status":"unavailable","error":"No agent model is configured."}}"#.utf8)
#"{"type":"res","id":"\#(id)","ok":true,"payload":{"ok":false,"status":"unavailable","error":"No agent model is configured."}}"#
.utf8)
}
private func unavailableGatewayResponse(id: String) -> Data {
@@ -2243,21 +2244,30 @@ struct OnboardingAISetupTests {
#expect(harness.session.latestTask()?.snapshotSendCount() == 1)
}
@Test func `configured gateway probe refuses an unpersisted endpoint selection`() async throws {
@Test(arguments: [false, true])
func `configured gateway probe refuses an unpersisted endpoint selection`(remote: Bool) async throws {
let url = try #require(URL(string: "ws://localhost:18789"))
let harness = AISetupHarness(url: url) { _, request, _ in configuredModelResponse(id: request.id) }
let harness = AISetupHarness(url: url) { _, request, _ in
switch request.method {
case "agents.list": missingConfiguredModelResponse(id: request.id)
case "openclaw.setup.detect": detectedSetupResponse(id: request.id)
default: nil
}
}
let appState = AppState(preview: true)
appState.connectionMode = .remote
appState.remoteTransport = .direct
appState.remoteUrl = "wss://replacement.example.test"
appState.connectionMode = remote ? .remote : .local
if remote {
appState.remoteTransport = .direct
appState.remoteUrl = "wss://replacement.example.test"
}
var persistAttempts = 0
let view = makeAISetupView(
state: appState,
gateway: harness.gateway,
routeIdentityProvider: { "remote:direct:replacement.example.test" },
routeIdentityProvider: { remote ? "remote:direct:replacement.example.test" : "local" },
gatewaySelectionPersister: {
persistAttempts += 1
return false
return persistAttempts > 2
})
view.onboardingVisible = true
@@ -2268,6 +2278,27 @@ struct OnboardingAISetupTests {
#expect(persistAttempts == 1)
#expect(harness.session.snapshotMakeCount() == 0)
#expect(!view.aiSetup.connected)
#expect(view.aiSetup.phase == .ready)
#expect(view.aiSetup.configuredGatewayProbeUnavailable)
#expect(view.aiSetup.detectError?.summary ==
"Could not save Gateway settings. Check your connection settings and try again.")
#expect(view.retryConfiguredGatewayProbe() == nil)
#expect(persistAttempts == 2)
#expect(harness.session.snapshotMakeCount() == 0)
#expect(view.aiSetup.phase == .ready)
#expect(view.aiSetup.configuredGatewayProbeUnavailable)
let retry = try #require(view.retryConfiguredGatewayProbe())
await retry.value
let requests = await waitForAISetupRequests(harness.recorder, count: 2)
await settleQueuedAISetupTasks()
#expect(persistAttempts == 3)
#expect(requests.methods == ["agents.list", "openclaw.setup.detect"])
#expect(view.aiSetup.phase == .ready)
#expect(!view.aiSetup.configuredGatewayProbeUnavailable)
#expect(!view.aiSetup.candidates.isEmpty)
}
@Test func `read only configured gateway retry does not own inference transition`() {
@@ -3072,16 +3103,16 @@ struct OnboardingAISetupTests {
let harness = AISetupHarness(url: url) { _, request, _ in
switch request.method {
case "openclaw.setup.detect":
return selectableCandidatesDetectedSetupResponse(id: request.id)
selectableCandidatesDetectedSetupResponse(id: request.id)
case "openclaw.setup.activate" where attempts.claim() < 2:
return failedActivationResponse(id: request.id)
failedActivationResponse(id: request.id)
case "openclaw.setup.activate":
return successfulActivationResponse(
successfulActivationResponse(
id: request.id,
modelRef: "openai/gpt-5.5",
latencyMs: 120)
default:
return nil
nil
}
}
let model = harness.model(defaults: defaults)