fix: clear stale remote discovery endpoints (#21618) (thanks @bmendonca3)

This commit is contained in:
Peter Steinberger
2026-02-21 23:52:38 +01:00
parent 37d5320f6b
commit bfe016fa29
8 changed files with 111 additions and 13 deletions

View File

@@ -2,6 +2,17 @@ import Foundation
import OpenClawDiscovery
enum GatewayDiscoveryHelpers {
static func resolvedServiceHost(
for gateway: GatewayDiscoveryModel.DiscoveredGateway) -> String?
{
self.resolvedServiceHost(gateway.serviceHost)
}
static func resolvedServiceHost(_ host: String?) -> String? {
guard let host = self.trimmed(host), !host.isEmpty else { return nil }
return host
}
static func serviceEndpoint(
for gateway: GatewayDiscoveryModel.DiscoveredGateway) -> (host: String, port: Int)?
{
@@ -12,15 +23,15 @@ enum GatewayDiscoveryHelpers {
serviceHost: String?,
servicePort: Int?) -> (host: String, port: Int)?
{
guard let host = self.trimmed(serviceHost), !host.isEmpty else { return nil }
guard let host = self.resolvedServiceHost(serviceHost) else { return nil }
guard let port = servicePort, port > 0, port <= 65535 else { return nil }
return (host, port)
}
static func sshTarget(for gateway: GatewayDiscoveryModel.DiscoveredGateway) -> String? {
guard let endpoint = self.serviceEndpoint(for: gateway) else { return nil }
guard let host = self.resolvedServiceHost(for: gateway) else { return nil }
let user = NSUserName()
var target = "\(user)@\(endpoint.host)"
var target = "\(user)@\(host)"
if gateway.sshPort != 22 {
target += ":\(gateway.sshPort)"
}

View File

@@ -676,16 +676,16 @@ extension GeneralSettings {
MacNodeModeCoordinator.shared.setPreferredGatewayStableID(gateway.stableID)
if self.state.remoteTransport == .direct {
if let url = GatewayDiscoveryHelpers.directUrl(for: gateway) {
self.state.remoteUrl = url
}
} else if let target = GatewayDiscoveryHelpers.sshTarget(for: gateway) {
self.state.remoteTarget = target
self.state.remoteUrl = GatewayDiscoveryHelpers.directUrl(for: gateway) ?? ""
} else {
self.state.remoteTarget = GatewayDiscoveryHelpers.sshTarget(for: gateway) ?? ""
}
if let endpoint = GatewayDiscoveryHelpers.serviceEndpoint(for: gateway) {
OpenClawConfigFile.setRemoteGatewayUrl(
host: endpoint.host,
port: endpoint.port)
} else {
OpenClawConfigFile.clearRemoteGatewayUrl()
}
}
}

View File

@@ -26,16 +26,16 @@ extension OnboardingView {
GatewayDiscoveryPreferences.setPreferredStableID(gateway.stableID)
if self.state.remoteTransport == .direct {
if let url = GatewayDiscoveryHelpers.directUrl(for: gateway) {
self.state.remoteUrl = url
}
} else if let target = GatewayDiscoveryHelpers.sshTarget(for: gateway) {
self.state.remoteTarget = target
self.state.remoteUrl = GatewayDiscoveryHelpers.directUrl(for: gateway) ?? ""
} else {
self.state.remoteTarget = GatewayDiscoveryHelpers.sshTarget(for: gateway) ?? ""
}
if let endpoint = GatewayDiscoveryHelpers.serviceEndpoint(for: gateway) {
OpenClawConfigFile.setRemoteGatewayUrl(
host: endpoint.host,
port: endpoint.port)
} else {
OpenClawConfigFile.clearRemoteGatewayUrl()
}
self.state.connectionMode = .remote

View File

@@ -223,6 +223,19 @@ enum OpenClawConfigFile {
}
}
static func clearRemoteGatewayUrl() {
self.updateGatewayDict { gateway in
guard var remote = gateway["remote"] as? [String: Any] else { return }
guard remote["url"] != nil else { return }
remote.removeValue(forKey: "url")
if remote.isEmpty {
gateway.removeValue(forKey: "remote")
} else {
gateway["remote"] = remote
}
}
}
private static func remoteGatewayUrl() -> URL? {
let root = self.loadDict()
guard let gateway = root["gateway"] as? [String: Any],

View File

@@ -42,6 +42,21 @@ struct GatewayDiscoveryHelpersTests {
#expect(parsed?.port == 2201)
}
@Test func sshTargetAllowsMissingResolvedServicePort() {
let gateway = self.makeGateway(
serviceHost: "resolved.example.ts.net",
servicePort: nil,
sshPort: 2201)
guard let target = GatewayDiscoveryHelpers.sshTarget(for: gateway) else {
Issue.record("expected ssh target")
return
}
let parsed = CommandResolver.parseSSHTarget(target)
#expect(parsed?.host == "resolved.example.ts.net")
#expect(parsed?.port == 2201)
}
@Test func sshTargetRejectsTxtOnlyGateways() {
let gateway = self.makeGateway(
serviceHost: nil,

View File

@@ -1,3 +1,4 @@
import Foundation
import OpenClawDiscovery
import SwiftUI
import Testing
@@ -25,4 +26,36 @@ struct OnboardingViewSmokeTests {
let order = OnboardingView.pageOrder(for: .local, showOnboardingChat: false)
#expect(!order.contains(8))
}
@Test func selectRemoteGatewayClearsStaleSshTargetWhenEndpointUnresolved() async {
let override = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-config-\(UUID().uuidString)")
.appendingPathComponent("openclaw.json")
.path
await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": override]) {
let state = AppState(preview: true)
state.remoteTransport = .ssh
state.remoteTarget = "user@old-host:2222"
let view = OnboardingView(
state: state,
permissionMonitor: PermissionMonitor.shared,
discoveryModel: GatewayDiscoveryModel(localDisplayName: InstanceIdentity.displayName))
let gateway = GatewayDiscoveryModel.DiscoveredGateway(
displayName: "Unresolved",
serviceHost: nil,
servicePort: nil,
lanHost: "txt-host.local",
tailnetDns: "txt-host.ts.net",
sshPort: 22,
gatewayPort: 18789,
cliPath: "/tmp/openclaw",
stableID: UUID().uuidString,
debugID: UUID().uuidString,
isLocal: false)
view.selectRemoteGateway(gateway)
#expect(state.remoteTarget.isEmpty)
}
}
}

View File

@@ -62,6 +62,31 @@ struct OpenClawConfigFileTests {
}
}
@MainActor
@Test
func clearRemoteGatewayUrlRemovesOnlyUrlField() async {
let override = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-config-\(UUID().uuidString)")
.appendingPathComponent("openclaw.json")
.path
await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": override]) {
OpenClawConfigFile.saveDict([
"gateway": [
"remote": [
"url": "wss://old-host:111",
"token": "tok",
],
],
])
OpenClawConfigFile.clearRemoteGatewayUrl()
let root = OpenClawConfigFile.loadDict()
let remote = ((root["gateway"] as? [String: Any])?["remote"] as? [String: Any]) ?? [:]
#expect((remote["url"] as? String) == nil)
#expect((remote["token"] as? String) == "tok")
}
}
@Test
func stateDirOverrideSetsConfigPath() async {
let dir = FileManager().temporaryDirectory