From 37e0b016842ef9e2bc02bf1667343c4b14d0c621 Mon Sep 17 00:00:00 2001 From: Charles Dusek Date: Wed, 4 Mar 2026 10:13:20 -0600 Subject: [PATCH] macos: add mode-toggle remote token sync coverage --- apps/macos/Sources/OpenClaw/AppState.swift | 138 +++++++++++------- .../AppStateRemoteConfigTests.swift | 40 +++++ 2 files changed, 129 insertions(+), 49 deletions(-) diff --git a/apps/macos/Sources/OpenClaw/AppState.swift b/apps/macos/Sources/OpenClaw/AppState.swift index e670235ddba..57d8ae87878 100644 --- a/apps/macos/Sources/OpenClaw/AppState.swift +++ b/apps/macos/Sources/OpenClaw/AppState.swift @@ -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() { guard !self.isPreview, !self.isInitializing else { return } @@ -517,58 +577,19 @@ final class AppState { let remoteTransport = self.remoteTransport let remoteUrl = self.remoteUrl 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 // Keep app-only connection settings local to avoid overwriting remote gateway config. - var root = OpenClawConfigFile.loadDict() - var gateway = root["gateway"] as? [String: Any] ?? [:] - var changed = false - - 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 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) + let synced = Self.syncedGatewayRoot( + currentRoot: OpenClawConfigFile.loadDict(), + connectionMode: connectionMode, + remoteTransport: remoteTransport, + remoteTarget: remoteTarget, + remoteIdentity: remoteIdentity, + remoteUrl: remoteUrl, + remoteToken: remoteToken) + guard synced.changed else { return } + OpenClawConfigFile.saveDict(synced.root) } } @@ -740,6 +761,25 @@ extension AppState { remoteIdentity: remoteIdentity, 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 diff --git a/apps/macos/Tests/OpenClawIPCTests/AppStateRemoteConfigTests.swift b/apps/macos/Tests/OpenClawIPCTests/AppStateRemoteConfigTests.swift index 8b903aa2a11..ac6d696ab4f 100644 --- a/apps/macos/Tests/OpenClawIPCTests/AppStateRemoteConfigTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/AppStateRemoteConfigTests.swift @@ -31,4 +31,44 @@ struct AppStateRemoteConfigTests { #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) + } }