macos: add mode-toggle remote token sync coverage

This commit is contained in:
Charles Dusek
2026-03-04 10:13:20 -06:00
committed by Nimrod Gutman
parent bd0e6a6efd
commit 37e0b01684
2 changed files with 129 additions and 49 deletions

View File

@@ -508,6 +508,66 @@ final class AppState {
} }
} }
private static func syncedGatewayRoot(
currentRoot: [String: Any],
connectionMode: ConnectionMode,
remoteTransport: RemoteTransport,
remoteTarget: String,
remoteIdentity: String,
remoteUrl: String,
remoteToken: String) -> (root: [String: Any], changed: Bool)
{
var root = currentRoot
var gateway = root["gateway"] as? [String: Any] ?? [:]
var changed = false
let desiredMode: String? = switch connectionMode {
case .local:
"local"
case .remote:
"remote"
case .unconfigured:
nil
}
let currentMode = (gateway["mode"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
if let desiredMode {
if currentMode != desiredMode {
gateway["mode"] = desiredMode
changed = true
}
} else if currentMode != nil {
gateway.removeValue(forKey: "mode")
changed = true
}
if connectionMode == .remote {
let remoteHost = CommandResolver.parseSSHTarget(remoteTarget)?.host
let currentRemote = gateway["remote"] as? [String: Any] ?? [:]
let updated = Self.updatedRemoteGatewayConfig(
current: currentRemote,
transport: remoteTransport,
remoteUrl: remoteUrl,
remoteHost: remoteHost,
remoteTarget: remoteTarget,
remoteIdentity: remoteIdentity,
remoteToken: remoteToken)
if updated.changed {
gateway["remote"] = updated.remote
changed = true
}
}
guard changed else { return (currentRoot, false) }
if gateway.isEmpty {
root.removeValue(forKey: "gateway")
} else {
root["gateway"] = gateway
}
return (root, true)
}
private func syncGatewayConfigIfNeeded() { private func syncGatewayConfigIfNeeded() {
guard !self.isPreview, !self.isInitializing else { return } guard !self.isPreview, !self.isInitializing else { return }
@@ -517,58 +577,19 @@ final class AppState {
let remoteTransport = self.remoteTransport let remoteTransport = self.remoteTransport
let remoteUrl = self.remoteUrl let remoteUrl = self.remoteUrl
let remoteToken = self.remoteToken let remoteToken = self.remoteToken
let desiredMode: String? = switch connectionMode {
case .local:
"local"
case .remote:
"remote"
case .unconfigured:
nil
}
let remoteHost = connectionMode == .remote
? CommandResolver.parseSSHTarget(remoteTarget)?.host
: nil
Task { @MainActor in Task { @MainActor in
// Keep app-only connection settings local to avoid overwriting remote gateway config. // Keep app-only connection settings local to avoid overwriting remote gateway config.
var root = OpenClawConfigFile.loadDict() let synced = Self.syncedGatewayRoot(
var gateway = root["gateway"] as? [String: Any] ?? [:] currentRoot: OpenClawConfigFile.loadDict(),
var changed = false connectionMode: connectionMode,
remoteTransport: remoteTransport,
let currentMode = (gateway["mode"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) remoteTarget: remoteTarget,
if let desiredMode { remoteIdentity: remoteIdentity,
if currentMode != desiredMode { remoteUrl: remoteUrl,
gateway["mode"] = desiredMode remoteToken: remoteToken)
changed = true guard synced.changed else { return }
} OpenClawConfigFile.saveDict(synced.root)
} else if currentMode != nil {
gateway.removeValue(forKey: "mode")
changed = true
}
if connectionMode == .remote {
let currentRemote = gateway["remote"] as? [String: Any] ?? [:]
let updated = Self.updatedRemoteGatewayConfig(
current: currentRemote,
transport: remoteTransport,
remoteUrl: remoteUrl,
remoteHost: remoteHost,
remoteTarget: remoteTarget,
remoteIdentity: remoteIdentity,
remoteToken: remoteToken)
if updated.changed {
gateway["remote"] = updated.remote
changed = true
}
}
guard changed else { return }
if gateway.isEmpty {
root.removeValue(forKey: "gateway")
} else {
root["gateway"] = gateway
}
OpenClawConfigFile.saveDict(root)
} }
} }
@@ -740,6 +761,25 @@ extension AppState {
remoteIdentity: remoteIdentity, remoteIdentity: remoteIdentity,
remoteToken: remoteToken).remote remoteToken: remoteToken).remote
} }
static func _testSyncedGatewayRoot(
currentRoot: [String: Any],
connectionMode: ConnectionMode,
remoteTransport: RemoteTransport,
remoteTarget: String,
remoteIdentity: String,
remoteUrl: String,
remoteToken: String) -> [String: Any]
{
Self.syncedGatewayRoot(
currentRoot: currentRoot,
connectionMode: connectionMode,
remoteTransport: remoteTransport,
remoteTarget: remoteTarget,
remoteIdentity: remoteIdentity,
remoteUrl: remoteUrl,
remoteToken: remoteToken).root
}
} }
#endif #endif

View File

@@ -31,4 +31,44 @@ struct AppStateRemoteConfigTests {
#expect((remote["token"] as? String) == nil) #expect((remote["token"] as? String) == nil)
} }
@Test
func syncedGatewayRootPreservesTokenAcrossModeToggleAndClearsOnBlankRemoteToken() {
let remoteRoot = AppState._testSyncedGatewayRoot(
currentRoot: [:],
connectionMode: .remote,
remoteTransport: .direct,
remoteTarget: "",
remoteIdentity: "",
remoteUrl: "wss://gateway.example",
remoteToken: " persisted-token ")
let remoteGateway = remoteRoot["gateway"] as? [String: Any]
let remoteConfig = remoteGateway?["remote"] as? [String: Any]
#expect(remoteGateway?["mode"] as? String == "remote")
#expect(remoteConfig?["token"] as? String == "persisted-token")
let localRoot = AppState._testSyncedGatewayRoot(
currentRoot: remoteRoot,
connectionMode: .local,
remoteTransport: .direct,
remoteTarget: "",
remoteIdentity: "",
remoteUrl: "",
remoteToken: "")
let localGateway = localRoot["gateway"] as? [String: Any]
let localRemoteConfig = localGateway?["remote"] as? [String: Any]
#expect(localGateway?["mode"] as? String == "local")
#expect(localRemoteConfig?["token"] as? String == "persisted-token")
let clearedRoot = AppState._testSyncedGatewayRoot(
currentRoot: localRoot,
connectionMode: .remote,
remoteTransport: .direct,
remoteTarget: "",
remoteIdentity: "",
remoteUrl: "wss://gateway.example",
remoteToken: " ")
let clearedRemote = (clearedRoot["gateway"] as? [String: Any])?["remote"] as? [String: Any]
#expect((clearedRemote?["token"] as? String) == nil)
}
} }