Files
openclaw/apps/macos/Tests/OpenClawIPCTests/AppStateRemoteConfigTests.swift
Peter Steinberger 96f0983a85 fix(onboarding): skip setup for configured gateways and require inference first (#102883)
* fix(crestodian): keep onboarding RPCs restart-safe

* fix(profiles): isolate approval state migrations

* fix(crestodian): bypass configured gateway setup

* test(crestodian): type onboarding mocks

* fix(onboarding): require inference before Crestodian

* fix(onboarding): enforce verified inference handoff

* fix(macos): reset setup on gateway endpoint edits

* chore(i18n): refresh native source inventory

* fix(gateway): keep socket on request cancellation

* test(packaging): require workspace templates

* fix(onboarding): bind setup to verified inference

* fix(onboarding): align inference gate contracts

* fix(crestodian): classify concurrent policy rejection

* test(crestodian): expect registry restoration

* fix(onboarding): bind setup to configured gateways

* fix(codex): preserve startup phase deadlines

* test(crestodian): match fail-closed policy ordering

* test(onboarding): assert bound gateway handoff

* fix(codex): bind runtime resolution to spawn cwd

* test(crestodian): assert policy rejection order

* fix(cli): preserve gateway routing across restarts

* fix(macos): fail closed during gateway edits

* test(macos): cover gateway route generation races

* chore: keep release notes out of onboarding PR

* fix(ci): refresh onboarding generated checks

* style(swift): align gateway channel formatting

* fix(ci): refresh plugin SDK surface budgets

* fix(ci): resync native string inventory

* refactor(swift): split gateway channel support

* test(doctor): isolate plugin compatibility registry

* test(macos): isolate gateway onboarding fixtures

* test(macos): assert gateway lease health ordering

* fix(codex): reconcile computer-use startup changes
2026-07-11 10:25:14 -07:00

768 lines
29 KiB
Swift

import Foundation
import Testing
@testable import OpenClaw
private struct StoredGatewayPreference {
let stableID: String?
let routeBinding: String?
}
private func captureGatewayPreference() -> StoredGatewayPreference {
StoredGatewayPreference(
stableID: GatewayDiscoveryPreferences.preferredStableID(),
routeBinding: GatewayDiscoveryPreferences.preferredRouteBinding())
}
private func restoreGatewayPreference(_ preference: StoredGatewayPreference) {
GatewayDiscoveryPreferences.setPreferredStableID(
preference.stableID,
routeBinding: preference.routeBinding)
}
private actor GatewayConfigReadGate {
private var started = false
private var startWaiters: [CheckedContinuation<Void, Never>] = []
private var releaseWaiter: CheckedContinuation<Void, Never>?
func suspendRead() async {
self.started = true
for waiter in self.startWaiters {
waiter.resume()
}
self.startWaiters.removeAll()
await withCheckedContinuation { continuation in
self.releaseWaiter = continuation
}
}
func waitUntilStarted() async {
guard !self.started else { return }
await withCheckedContinuation { continuation in
self.startWaiters.append(continuation)
}
}
func release() {
self.releaseWaiter?.resume()
self.releaseWaiter = nil
}
}
@Suite(.serialized)
@MainActor
struct AppStateRemoteConfigTests {
@Test
func `route edit during config read fails the source snapshot closed`() async {
let configPath = TestIsolation.tempConfigPath()
await TestIsolation.withIsolatedState(
env: ["OPENCLAW_CONFIG_PATH": configPath],
defaults: [connectionModeKey: AppState.ConnectionMode.remote.rawValue])
{
#expect(OpenClawConfigFile.saveDict([
"gateway": [
"mode": "remote",
"remote": [
"transport": "direct",
"url": "wss://gateway-a.example.test",
"token": "route-a-token",
],
],
]))
let state = AppState(preview: true)
let gate = GatewayConfigReadGate()
let read = Task {
await GatewayEndpointStore._testLiveSourceSnapshot(
state: state,
beforeConfigRead: { await gate.suspendRead() })
}
await gate.waitUntilStarted()
state.remoteUrl = "wss://gateway-b.example.test"
await gate.release()
let source = await read.value
#expect(source.mode == .unconfigured)
#expect(source.token == nil)
#expect(source.password == nil)
#expect(source.deviceAuthGatewayID == nil)
#expect(source.directRemoteURL == nil)
#expect(source.sshRouteIdentity == nil)
}
}
@Test
func `unbound discovery selection cannot inherit a prior route binding`() {
let previousGatewayPreference = captureGatewayPreference()
defer { restoreGatewayPreference(previousGatewayPreference) }
GatewayDiscoveryPreferences.setPreferredStableID(
"gateway-a",
routeBinding: "remote:direct:wss://gateway-a.example.test:443")
GatewayDiscoveryPreferences.setPreferredStableID("gateway-b")
#expect(GatewayDiscoveryPreferences.preferredStableID() == "gateway-b")
#expect(GatewayDiscoveryPreferences.preferredRouteBinding() == nil)
}
@Test
func `invalid remote drafts cannot be persisted for a configured gateway probe`() {
let base = AppState.GatewayConfigSyncDraft(
connectionMode: .remote,
remoteTransport: .direct,
remoteTarget: "",
remoteIdentity: "",
remoteUrl: "not a gateway URL",
remoteToken: "",
remoteTokenDirty: false)
#expect(!AppState._testGatewayDraftCanPersist(base))
#expect(AppState._testGatewayDraftCanPersist(.init(
connectionMode: .remote,
remoteTransport: .direct,
remoteTarget: "",
remoteIdentity: "",
remoteUrl: "wss://gateway.example.test",
remoteToken: "",
remoteTokenDirty: false)))
#expect(!AppState._testGatewayDraftCanPersist(.init(
connectionMode: .remote,
remoteTransport: .ssh,
remoteTarget: "",
remoteIdentity: "",
remoteUrl: "ws://127.0.0.1:18789",
remoteToken: "",
remoteTokenDirty: false)))
}
@Test
func `invalid remote edit retires the prior canonical gateway route`() async {
let configPath = TestIsolation.tempConfigPath()
await TestIsolation.withIsolatedState(
env: ["OPENCLAW_CONFIG_PATH": configPath],
defaults: [connectionModeKey: AppState.ConnectionMode.remote.rawValue])
{
#expect(OpenClawConfigFile.saveDict([
"gateway": [
"mode": "remote",
"remote": [
"transport": "ssh",
"url": "ws://127.0.0.1:18789",
"sshTarget": "alice@gateway-a.example.test",
],
],
]))
let state = AppState(preview: true)
state._testEnableGatewayConfigSync()
state.remoteTarget = ""
#expect(!state._testGatewayConfigIsCurrentForRouting)
await state._testAwaitGatewayConfigSync()
#expect(!state._testGatewayConfigIsCurrentForRouting)
let persisted = CommandResolver.connectionSettings()
#expect(persisted.target == "alice@gateway-a.example.test")
#expect(GatewayEndpointStore._testEffectiveSourceMode(
appMode: .remote,
configMode: .remote,
configIsCurrent: state._testGatewayConfigIsCurrentForRouting) == .unconfigured)
}
}
@Test
func `remote identity edit updates canonical SSH config before routing resumes`() async {
let configPath = TestIsolation.tempConfigPath()
await TestIsolation.withIsolatedState(
env: ["OPENCLAW_CONFIG_PATH": configPath],
defaults: [connectionModeKey: AppState.ConnectionMode.remote.rawValue])
{
#expect(OpenClawConfigFile.saveDict([
"gateway": [
"mode": "remote",
"remote": [
"transport": "ssh",
"url": "ws://127.0.0.1:18789",
"sshTarget": "alice@gateway.example.test",
"sshIdentity": "/tmp/old-identity",
],
],
]))
let state = AppState(preview: true)
state._testEnableGatewayConfigSync()
state.remoteIdentity = " /tmp/new-identity "
#expect(!state._testGatewayConfigIsCurrentForRouting)
await state._testAwaitGatewayConfigSync()
#expect(state._testGatewayConfigIsCurrentForRouting)
let persisted = CommandResolver.connectionSettings()
#expect(persisted.identity == "/tmp/new-identity")
let remote = (OpenClawConfigFile.loadDict()["gateway"] as? [String: Any])?["remote"]
as? [String: Any]
#expect(remote?["sshIdentity"] as? String == "/tmp/new-identity")
}
}
@Test
func `config watcher endpoint replacement clears and ignores stale discovery identity`() {
let previousGatewayPreference = captureGatewayPreference()
let previousPending = UserDefaults.standard.object(forKey: onboardingCrestodianPendingKey)
defer {
restoreGatewayPreference(previousGatewayPreference)
if let previousPending {
UserDefaults.standard.set(previousPending, forKey: onboardingCrestodianPendingKey)
} else {
OnboardingCrestodianResumeStore.clear()
}
}
let state = AppState(preview: true)
state.connectionMode = .remote
state.remoteTransport = .direct
state.remoteUrl = "wss://gateway-a.example.test"
GatewayDiscoveryPreferences.setPreferredStableID("gateway-a")
OnboardingCrestodianResumeStore.markPending(routeIdentity: "remote:id:gateway-a")
let view = OnboardingView(state: state)
view.preferredGatewayID = "gateway-a"
state._testApplyConfigOverrides([
"gateway": [
"mode": "remote",
"remote": [
"transport": "direct",
"url": "wss://gateway-b.example.test",
],
],
])
#expect(state.remoteUrl == "wss://gateway-b.example.test")
#expect(GatewayDiscoveryPreferences.preferredStableID() == nil)
#expect(view.effectivePreferredGatewayID == nil)
let routeIdentity = OnboardingCrestodianResumeStore.selectedRouteIdentity(
state: state,
preferredGatewayID: view.effectivePreferredGatewayID)
#expect(routeIdentity?.hasPrefix("remote:direct:") == true)
#expect(routeIdentity != "remote:id:gateway-a")
#expect(!OnboardingCrestodianResumeStore.isPending(for: routeIdentity))
#expect(OnboardingCrestodianResumeStore.isPending(for: "remote:id:gateway-a"))
}
@Test
func `config watcher explicit ssh target replacement clears stale discovery identity`() {
let previousGatewayPreference = captureGatewayPreference()
defer { restoreGatewayPreference(previousGatewayPreference) }
let state = AppState(preview: true)
state.connectionMode = .remote
state.remoteTransport = .ssh
state.remoteUrl = "ws://127.0.0.1:18789"
state.remoteTarget = "alice@gateway-a.example.test"
GatewayDiscoveryPreferences.setPreferredStableID("gateway-a")
let view = OnboardingView(state: state)
view.preferredGatewayID = "gateway-a"
state._testApplyConfigOverrides([
"gateway": [
"mode": "remote",
"remote": [
"transport": "ssh",
"url": "ws://127.0.0.1:18789",
"sshTarget": "bob@gateway-b.example.test",
],
],
])
#expect(state.remoteTarget == "bob@gateway-b.example.test")
#expect(GatewayDiscoveryPreferences.preferredStableID() == nil)
#expect(view.effectivePreferredGatewayID == nil)
}
@Test
func `config watcher explicit blank SSH fields clear stale defaults`() {
let previousGatewayPreference = captureGatewayPreference()
defer { restoreGatewayPreference(previousGatewayPreference) }
let state = AppState(preview: true)
state.connectionMode = .remote
state.remoteTransport = .ssh
state.remoteUrl = "ws://127.0.0.1:18789"
state.remoteTarget = "alice@gateway-a.example.test"
state.remoteIdentity = "/tmp/gateway-a-id"
GatewayDiscoveryPreferences.setPreferredStableID("gateway-a")
state._testApplyConfigOverrides([
"gateway": [
"mode": "remote",
"remote": [
"transport": "ssh",
"url": "ws://127.0.0.1:18789",
"sshTarget": " ",
"sshIdentity": " ",
],
],
])
#expect(state.remoteTarget.isEmpty)
#expect(state.remoteIdentity.isEmpty)
#expect(GatewayDiscoveryPreferences.preferredStableID() == nil)
}
@Test
func `cold direct config replacement clears the prior discovery owner`() async {
let configPath = TestIsolation.tempConfigPath()
let previousGatewayPreference = captureGatewayPreference()
defer { restoreGatewayPreference(previousGatewayPreference) }
await TestIsolation.withIsolatedState(
env: ["OPENCLAW_CONFIG_PATH": configPath],
defaults: [connectionModeKey: AppState.ConnectionMode.remote.rawValue])
{
#expect(OpenClawConfigFile.saveDict([
"gateway": [
"mode": "remote",
"remote": [
"transport": "direct",
"url": "wss://gateway-b.example.test",
],
],
]))
let oldBinding = GatewayDiscoveryPreferences.routeBinding(
connectionMode: .remote,
remoteTransport: .direct,
remoteURL: "wss://gateway-a.example.test",
remoteTarget: "")
GatewayDiscoveryPreferences.setPreferredStableID(
"gateway-a",
routeBinding: oldBinding)
let state = AppState(preview: true)
#expect(state.remoteUrl == "wss://gateway-b.example.test")
#expect(state._testReconcilePreferredGatewayRouteBinding())
#expect(GatewayDiscoveryPreferences.preferredStableID() == nil)
#expect(GatewayDiscoveryPreferences.preferredRouteBinding() == nil)
}
}
@Test
func `cold SSH config replacement overrides stale defaults and clears their owner`() async {
let configPath = TestIsolation.tempConfigPath()
let previousGatewayPreference = captureGatewayPreference()
defer { restoreGatewayPreference(previousGatewayPreference) }
await TestIsolation.withIsolatedState(
env: ["OPENCLAW_CONFIG_PATH": configPath],
defaults: [
connectionModeKey: AppState.ConnectionMode.remote.rawValue,
remoteTargetKey: "alice@gateway-a.example.test",
remoteIdentityKey: "/tmp/gateway-a-id",
]) {
#expect(OpenClawConfigFile.saveDict([
"gateway": [
"mode": "remote",
"remote": [
"transport": "ssh",
"url": "ws://127.0.0.1:18789",
"sshTarget": "bob@gateway-b.example.test",
"sshIdentity": "/tmp/gateway-b-id",
],
],
]))
let oldBinding = GatewayDiscoveryPreferences.routeBinding(
connectionMode: .remote,
remoteTransport: .ssh,
remoteURL: "ws://127.0.0.1:18789",
remoteTarget: "alice@gateway-a.example.test")
GatewayDiscoveryPreferences.setPreferredStableID(
"gateway-a",
routeBinding: oldBinding)
let state = AppState(preview: true)
let settings = CommandResolver.connectionSettings()
#expect(state.remoteTarget == "bob@gateway-b.example.test")
#expect(state.remoteIdentity == "/tmp/gateway-b-id")
#expect(settings.target == "bob@gateway-b.example.test")
#expect(settings.identity == "/tmp/gateway-b-id")
#expect(state._testReconcilePreferredGatewayRouteBinding())
#expect(GatewayDiscoveryPreferences.preferredStableID() == nil)
#expect(GatewayDiscoveryPreferences.preferredRouteBinding() == nil)
}
}
@Test
func `cold explicit blank SSH fields clear stale defaults`() async {
let configPath = TestIsolation.tempConfigPath()
let previousGatewayPreference = captureGatewayPreference()
defer { restoreGatewayPreference(previousGatewayPreference) }
await TestIsolation.withIsolatedState(
env: ["OPENCLAW_CONFIG_PATH": configPath],
defaults: [
connectionModeKey: AppState.ConnectionMode.remote.rawValue,
remoteTargetKey: "alice@gateway-a.example.test",
remoteIdentityKey: "/tmp/gateway-a-id",
]) {
#expect(OpenClawConfigFile.saveDict([
"gateway": [
"mode": "remote",
"remote": [
"transport": "ssh",
"url": "ws://127.0.0.1:18789",
"sshTarget": " ",
"sshIdentity": " ",
],
],
]))
let state = AppState(preview: true)
let settings = CommandResolver.connectionSettings()
#expect(state.remoteTarget.isEmpty)
#expect(state.remoteIdentity.isEmpty)
#expect(settings.target.isEmpty)
#expect(settings.identity.isEmpty)
}
}
@Test
func `updated remote gateway config sets trimmed token`() {
let remote = AppState._testUpdatedRemoteGatewayConfig(
current: [:],
draft: .init(
transport: .ssh,
remoteUrl: "",
remoteHost: "gateway.example",
remoteTarget: "alice@gateway.example",
remoteIdentity: "/tmp/id_ed25519",
remoteToken: " secret-token ",
remoteTokenDirty: true))
#expect(remote["token"] as? String == "secret-token")
}
@Test
func `updated remote gateway config clears token when blank`() {
let remote = AppState._testUpdatedRemoteGatewayConfig(
current: ["token": "old-token"],
draft: .init(
transport: .direct,
remoteUrl: "wss://gateway.example",
remoteHost: nil,
remoteTarget: "",
remoteIdentity: "",
remoteToken: " ",
remoteTokenDirty: true))
#expect((remote["token"] as? String) == nil)
}
@Test
func `updated remote gateway config pins loopback url for ssh transport`() {
let remote = AppState._testUpdatedRemoteGatewayConfig(
current: ["url": "ws://gateway.example:18789"],
draft: .init(
transport: .ssh,
remoteUrl: "",
remoteHost: "gateway.example",
remoteTarget: "alice@gateway.example",
remoteIdentity: "",
remoteToken: "",
remoteTokenDirty: false))
#expect(remote["url"] as? String == "ws://127.0.0.1:18789")
#expect(remote["transport"] as? String == "ssh")
#expect(remote["sshTarget"] as? String == "alice@gateway.example")
}
@Test
func `updated remote gateway config keeps OpenSSH opt in only for the same target`() {
let sameTarget = AppState._testUpdatedRemoteGatewayConfig(
current: [
"sshHostKeyPolicy": "openssh",
"sshTarget": "alice@gateway.example",
],
draft: .init(
transport: .ssh,
remoteUrl: "",
remoteHost: nil,
remoteTarget: "alice@gateway.example",
remoteIdentity: "",
remoteToken: "",
remoteTokenDirty: false))
let changedTarget = AppState._testUpdatedRemoteGatewayConfig(
current: [
"sshHostKeyPolicy": "openssh",
"sshTarget": "old-gateway-alias",
],
draft: .init(
transport: .ssh,
remoteUrl: "",
remoteHost: nil,
remoteTarget: "new-gateway-alias",
remoteIdentity: "",
remoteToken: "",
remoteTokenDirty: false))
#expect(sameTarget["sshHostKeyPolicy"] as? String == "openssh")
#expect(changedTarget["sshHostKeyPolicy"] as? String == "strict")
}
@Test
func `updated remote gateway config preserves custom loopback tunnel port`() {
let remote = AppState._testUpdatedRemoteGatewayConfig(
current: ["url": "ws://localhost.:29876"],
draft: .init(
transport: .ssh,
remoteUrl: "",
remoteHost: "gateway.example",
remoteTarget: "alice@gateway.example",
remoteIdentity: "",
remoteToken: "",
remoteTokenDirty: false))
#expect(remote["url"] as? String == "ws://127.0.0.1:29876")
}
@Test
func `updated remote gateway config preserves custom port when existing host matches ssh target`() {
let remote = AppState._testUpdatedRemoteGatewayConfig(
current: ["url": "ws://gateway.example:19999"],
draft: .init(
transport: .ssh,
remoteUrl: "",
remoteHost: nil,
remoteTarget: "alice@gateway.example",
remoteIdentity: "",
remoteToken: "",
remoteTokenDirty: false))
#expect(remote["url"] as? String == "ws://127.0.0.1:19999")
}
@Test
func `updated remote gateway config drops custom port when existing host does not match ssh target`() {
let remote = AppState._testUpdatedRemoteGatewayConfig(
current: ["url": "ws://other-host.example:19999"],
draft: .init(
transport: .ssh,
remoteUrl: "",
remoteHost: "gateway.example",
remoteTarget: "alice@gateway.example",
remoteIdentity: "",
remoteToken: "",
remoteTokenDirty: false))
#expect(remote["url"] as? String == "ws://127.0.0.1:18789")
}
@Test
func `updated remote gateway config does not preserve port for hostname prefix collision`() {
let remote = AppState._testUpdatedRemoteGatewayConfig(
current: ["url": "ws://example.attacker.tld:19999"],
draft: .init(
transport: .ssh,
remoteUrl: "",
remoteHost: nil,
remoteTarget: "alice@example.com",
remoteIdentity: "",
remoteToken: "",
remoteTokenDirty: false))
#expect(remote["url"] as? String == "ws://127.0.0.1:18789")
}
@Test
func `app state init does not infer loopback host into remote target`() async {
let configPath = TestIsolation.tempConfigPath()
await TestIsolation.withIsolatedState(
env: ["OPENCLAW_CONFIG_PATH": configPath],
defaults: [remoteTargetKey: nil])
{
OpenClawConfigFile.saveDict([
"gateway": [
"mode": "remote",
"remote": [
"url": "ws://127.0.0.1:19999",
],
],
])
let state = AppState(preview: true)
#expect(state.remoteTarget.isEmpty)
}
}
@Test
func `app state init preserves existing remote target when remote url is loopback`() async {
let configPath = TestIsolation.tempConfigPath()
await TestIsolation.withIsolatedState(
env: ["OPENCLAW_CONFIG_PATH": configPath],
defaults: [remoteTargetKey: "alice@gateway.example"])
{
OpenClawConfigFile.saveDict([
"gateway": [
"mode": "remote",
"remote": [
"url": "ws://127.0.0.1:19999",
],
],
])
let state = AppState(preview: true)
#expect(state.remoteTarget == "alice@gateway.example")
}
}
@Test
func `app state init preserves legacy SSH tunnel config until transport is explicit`() async {
let configPath = TestIsolation.tempConfigPath()
await TestIsolation.withIsolatedState(
env: ["OPENCLAW_CONFIG_PATH": configPath],
defaults: [remoteTargetKey: nil])
{
OpenClawConfigFile.saveDict([
"gateway": [
"mode": "remote",
"remote": [
"url": "ws://127.0.0.1:18789",
"sshTarget": "steipete@192.168.0.202",
],
],
])
let state = AppState(preview: true)
#expect(state.remoteTransport == .ssh)
#expect(state.remoteUrl == "ws://127.0.0.1:18789")
}
}
@Test
func `synced gateway root preserves object token across mode and transport changes when untouched`() {
let initialRoot: [String: Any] = [
"gateway": [
"mode": "remote",
"remote": [
"transport": "direct",
"url": "wss://old-gateway.example",
"token": [
"$secretRef": "gateway-token", // pragma: allowlist secret
],
],
],
]
let sshRoot = AppState._testSyncedGatewayRoot(
currentRoot: initialRoot,
draft: .init(
connectionMode: .remote,
remoteTransport: .ssh,
remoteTarget: "alice@gateway.example",
remoteIdentity: "",
remoteUrl: "",
remoteToken: "",
remoteTokenDirty: false))
let sshRemote = (sshRoot["gateway"] as? [String: Any])?["remote"] as? [String: Any]
#expect((sshRemote?["token"] as? [String: String])?["$secretRef"] ==
"gateway-token") // pragma: allowlist secret
let localRoot = AppState._testSyncedGatewayRoot(
currentRoot: sshRoot,
draft: .init(
connectionMode: .local,
remoteTransport: .ssh,
remoteTarget: "",
remoteIdentity: "",
remoteUrl: "",
remoteToken: "",
remoteTokenDirty: false))
let localGateway = localRoot["gateway"] as? [String: Any]
let localRemote = localGateway?["remote"] as? [String: Any]
#expect(localGateway?["mode"] as? String == "local")
#expect((localRemote?["token"] as? [String: String])?["$secretRef"] ==
"gateway-token") // pragma: allowlist secret
}
@Test
func `updated remote gateway config replaces object token when user enters plaintext`() {
let remote = AppState._testUpdatedRemoteGatewayConfig(
current: [
"token": [
"$secretRef": "gateway-token", // pragma: allowlist secret
],
],
draft: .init(
transport: .direct,
remoteUrl: "wss://gateway.example",
remoteHost: nil,
remoteTarget: "",
remoteIdentity: "",
remoteToken: " fresh-token ",
remoteTokenDirty: true))
#expect(remote["token"] as? String == "fresh-token")
}
@Test
func `updated remote gateway config clears object token only after explicit edit`() {
let current: [String: Any] = [
"token": [
"$secretRef": "gateway-token", // pragma: allowlist secret
],
]
let preserved = AppState._testUpdatedRemoteGatewayConfig(
current: current,
draft: .init(
transport: .direct,
remoteUrl: "wss://gateway.example",
remoteHost: nil,
remoteTarget: "",
remoteIdentity: "",
remoteToken: "",
remoteTokenDirty: false))
#expect((preserved["token"] as? [String: String])?["$secretRef"] == "gateway-token") // pragma: allowlist secret
let cleared = AppState._testUpdatedRemoteGatewayConfig(
current: current,
draft: .init(
transport: .direct,
remoteUrl: "wss://gateway.example",
remoteHost: nil,
remoteTarget: "",
remoteIdentity: "",
remoteToken: " ",
remoteTokenDirty: true))
#expect((cleared["token"] as? String) == nil)
}
@Test
func `synced gateway root preserves gateway auth across mode changes`() {
let initialRoot: [String: Any] = [
"gateway": [
"mode": "remote",
"auth": [
"mode": "token",
"token": "test-token", // pragma: allowlist secret
],
"remote": [
"transport": "direct",
"url": "wss://old-gateway.example",
],
],
]
let localRoot = AppState._testSyncedGatewayRoot(
currentRoot: initialRoot,
draft: .init(
connectionMode: .local,
remoteTransport: .ssh,
remoteTarget: "",
remoteIdentity: "",
remoteUrl: "",
remoteToken: "",
remoteTokenDirty: false))
let localGateway = localRoot["gateway"] as? [String: Any]
let auth = localGateway?["auth"] as? [String: Any]
#expect(localGateway?["mode"] as? String == "local")
#expect(auth?["mode"] as? String == "token")
#expect(auth?["token"] as? String == "test-token") // pragma: allowlist secret
}
}