openclaw/apps/macos/Tests/OpenClawIPCTests/OpenClawConfigFileTests.swift
Peter Steinberger 76154d9275
test(macos): deflake browser proxy gate tests via injectable browser control (#100517)
MacNodeRuntime's browser proxy gate read OPENCLAW_CONFIG_PATH-resolved config
at invoke time, so parallel tests mutating process env (raw setenv outside
TestIsolationLock in LogLocatorTests/WideAreaGatewayDiscoveryTests) could make
the gate read the real host config. Inject the gate state in tests, cover the
config flag parsing in OpenClawConfigFileTests, and route remaining raw setenv
test users through TestIsolation.withEnvValues.
2026-07-06 02:13:05 +01:00

522 lines
21 KiB
Swift

import Foundation
import Testing
@testable import OpenClaw
@Suite(.serialized)
struct OpenClawConfigFileTests {
private func makeConfigOverridePath() -> String {
FileManager().temporaryDirectory
.appendingPathComponent("openclaw-config-\(UUID().uuidString)")
.appendingPathComponent("openclaw.json")
.path
}
@Test
func `config path respects env override`() async {
let override = self.makeConfigOverridePath()
await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": override]) {
#expect(OpenClawConfigFile.url().path == override)
}
}
@MainActor
@Test
func `browser control enabled reads config flag`() async {
let override = self.makeConfigOverridePath()
await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": override]) {
#expect(OpenClawConfigFile.browserControlEnabled() == true)
OpenClawConfigFile.saveDict(["browser": ["enabled": false]])
#expect(OpenClawConfigFile.browserControlEnabled() == false)
OpenClawConfigFile.setBrowserControlEnabled(true)
#expect(OpenClawConfigFile.browserControlEnabled() == true)
}
}
@MainActor
@Test
func `remote gateway port parses and matches host`() async {
let override = self.makeConfigOverridePath()
await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": override]) {
OpenClawConfigFile.saveDict([
"gateway": [
"remote": [
"url": "ws://gateway.ts.net:19999",
],
],
])
#expect(OpenClawConfigFile.remoteGatewayPort() == 19999)
#expect(OpenClawConfigFile.remoteGatewayPort(matchingHost: "gateway.ts.net") == 19999)
#expect(OpenClawConfigFile.remoteGatewayPort(matchingHost: "GATEWAY.ts.net.") == 19999)
#expect(OpenClawConfigFile.remoteGatewayPort(matchingHost: "gateway") == nil)
#expect(OpenClawConfigFile.remoteGatewayPort(matchingHost: "other.ts.net") == nil)
#expect(OpenClawConfigFile.remoteGatewayPort(matchingHost: "gateway.attacker.tld") == nil)
}
}
@MainActor
@Test
func `set remote gateway url string replaces scheme`() async {
let override = self.makeConfigOverridePath()
await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": override]) {
OpenClawConfigFile.saveDict([
"gateway": [
"remote": [
"url": "wss://old-host:111",
],
],
])
OpenClawConfigFile.setRemoteGatewayUrlString("ws://127.0.0.1:18789")
let root = OpenClawConfigFile.loadDict()
let url = ((root["gateway"] as? [String: Any])?["remote"] as? [String: Any])?["url"] as? String
#expect(url == "ws://127.0.0.1:18789")
}
}
@MainActor
@Test
func `set remote gateway url preserves scheme`() async {
let override = self.makeConfigOverridePath()
await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": override]) {
OpenClawConfigFile.saveDict([
"gateway": [
"remote": [
"url": "wss://old-host:111",
],
],
])
OpenClawConfigFile.setRemoteGatewayUrl(host: "new-host", port: 2222)
let root = OpenClawConfigFile.loadDict()
let url = ((root["gateway"] as? [String: Any])?["remote"] as? [String: Any])?["url"] as? String
#expect(url == "wss://new-host:2222")
}
}
@MainActor
@Test
func `clear remote gateway url removes only url field`() async {
let override = self.makeConfigOverridePath()
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 `state dir override sets config path`() async {
let dir = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
.path
await TestIsolation.withEnvValues([
"OPENCLAW_CONFIG_PATH": nil,
"OPENCLAW_STATE_DIR": dir,
]) {
#expect(OpenClawConfigFile.stateDirURL().path == dir)
#expect(OpenClawConfigFile.url().path == "\(dir)/openclaw.json")
}
}
@MainActor
@Test
func `save dict appends config audit log`() async throws {
let stateDir = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
let configPath = stateDir.appendingPathComponent("openclaw.json")
let auditPath = stateDir.appendingPathComponent("logs/config-audit.jsonl")
defer { try? FileManager().removeItem(at: stateDir) }
try await TestIsolation.withEnvValues([
"OPENCLAW_STATE_DIR": stateDir.path,
"OPENCLAW_CONFIG_PATH": configPath.path,
]) {
OpenClawConfigFile.saveDict([
"gateway": ["mode": "local"],
])
let configData = try Data(contentsOf: configPath)
let configRoot = try JSONSerialization.jsonObject(with: configData) as? [String: Any]
#expect((configRoot?["meta"] as? [String: Any]) != nil)
let rawAudit = try String(contentsOf: auditPath, encoding: .utf8)
let lines = rawAudit
.split(whereSeparator: \.isNewline)
.map(String.init)
#expect(!lines.isEmpty)
guard let last = lines.last else {
Issue.record("Missing config audit line")
return
}
let auditRoot = try JSONSerialization.jsonObject(with: Data(last.utf8)) as? [String: Any]
#expect(auditRoot?["source"] as? String == "macos-openclaw-config-file")
#expect(auditRoot?["event"] as? String == "config.write")
#expect(auditRoot?["result"] as? String == "success")
#expect(auditRoot?["configPath"] as? String == configPath.path)
#expect(auditRoot?["previousMode"] is NSNull)
#expect(auditRoot?["nextMode"] is NSNumber)
#expect(auditRoot?["previousIno"] is NSNull)
#expect(auditRoot?["nextIno"] as? String != nil)
}
}
@MainActor
@Test
func `save dict preserves gateway auth unless explicitly allowed`() async throws {
let stateDir = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
let configPath = stateDir.appendingPathComponent("openclaw.json")
defer { try? FileManager().removeItem(at: stateDir) }
await TestIsolation.withEnvValues([
"OPENCLAW_STATE_DIR": stateDir.path,
"OPENCLAW_CONFIG_PATH": configPath.path,
]) {
OpenClawConfigFile.saveDict([
"gateway": [
"mode": "remote",
"auth": [
"mode": "token",
"token": "existing-token", // pragma: allowlist secret
],
],
])
OpenClawConfigFile.saveDict([
"gateway": [
"mode": "local",
],
])
let root = OpenClawConfigFile.loadDict()
let gateway = root["gateway"] as? [String: Any]
let auth = gateway?["auth"] as? [String: Any]
#expect(gateway?["mode"] as? String == "local")
#expect(auth?["mode"] as? String == "token")
#expect(auth?["token"] as? String == "existing-token") // pragma: allowlist secret
OpenClawConfigFile.saveDict([
"gateway": [
"mode": "local",
],
], allowGatewayAuthMutation: true)
let allowedRoot = OpenClawConfigFile.loadDict()
let allowedGateway = allowedRoot["gateway"] as? [String: Any]
#expect(allowedGateway?["mode"] as? String == "local")
#expect((allowedGateway?["auth"] as? [String: Any]) == nil)
}
}
@MainActor
@Test
func `save dict can merge local fallback writes with fresh config`() async throws {
let stateDir = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
let configPath = stateDir.appendingPathComponent("openclaw.json")
defer { try? FileManager().removeItem(at: stateDir) }
await TestIsolation.withEnvValues([
"OPENCLAW_STATE_DIR": stateDir.path,
"OPENCLAW_CONFIG_PATH": configPath.path,
]) {
OpenClawConfigFile.saveDict([
"gateway": [
"mode": "remote",
"auth": [
"mode": "password",
"password": "existing-password", // pragma: allowlist secret
],
],
"browser": [
"enabled": true,
"profile": "work",
],
"channels": [
"discord": [
"enabled": true,
],
],
])
OpenClawConfigFile.saveDict([
"gateway": [
"mode": "local",
],
"browser": [
"enabled": false,
],
], preserveExistingKeys: true)
let root = OpenClawConfigFile.loadDict()
let gateway = root["gateway"] as? [String: Any]
let auth = gateway?["auth"] as? [String: Any]
let browser = root["browser"] as? [String: Any]
let discord = ((root["channels"] as? [String: Any])?["discord"] as? [String: Any])
#expect(gateway?["mode"] as? String == "local")
#expect(auth?["mode"] as? String == "password")
#expect(auth?["password"] as? String == "existing-password") // pragma: allowlist secret
#expect(browser?["enabled"] as? Bool == false)
#expect(browser?["profile"] as? String == "work")
#expect(discord?["enabled"] as? Bool == true)
}
}
@MainActor
@Test
func `load dict ignores legacy config health sidecar`() async throws {
let stateDir = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
let configPath = stateDir.appendingPathComponent("openclaw.json")
let auditPath = stateDir.appendingPathComponent("logs/config-audit.jsonl")
let configHealthPath = stateDir.appendingPathComponent("logs/config-health.json")
defer { try? FileManager().removeItem(at: stateDir) }
try FileManager().createDirectory(
at: configHealthPath.deletingLastPathComponent(),
withIntermediateDirectories: true)
let legacyHealth = """
{
"entries": {
"\(configPath.path)": {
"lastKnownGood": {
"bytes": 4096,
"gatewayMode": "local",
"hasMeta": true
}
}
}
}
"""
try legacyHealth.write(to: configHealthPath, atomically: true, encoding: .utf8)
let updateOnlyConfig = """
{
"update": {
"channel": "beta"
}
}
"""
try updateOnlyConfig.write(to: configPath, atomically: true, encoding: .utf8)
try await TestIsolation.withEnvValues([
"OPENCLAW_STATE_DIR": stateDir.path,
"OPENCLAW_CONFIG_PATH": configPath.path,
]) {
try OpenClawConfigFile.withTestingFileLock {
let loaded = OpenClawConfigFile.loadDict()
let update = loaded["update"] as? [String: Any]
#expect(update?["channel"] as? String == "beta")
#expect(!FileManager().fileExists(atPath: auditPath.path))
let persistedHealth = try String(contentsOf: configHealthPath, encoding: .utf8)
#expect(persistedHealth == legacyHealth)
}
}
}
@MainActor
@Test
func `load dict audits suspicious out-of-band clobbers`() async throws {
let stateDir = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
let configPath = stateDir.appendingPathComponent("openclaw.json")
let auditPath = stateDir.appendingPathComponent("logs/config-audit.jsonl")
let configHealthPath = stateDir.appendingPathComponent("logs/config-health.json")
defer { try? FileManager().removeItem(at: stateDir) }
try await TestIsolation.withEnvValues([
"OPENCLAW_STATE_DIR": stateDir.path,
"OPENCLAW_CONFIG_PATH": configPath.path,
]) {
try OpenClawConfigFile.withTestingFileLock {
OpenClawConfigFile.saveDict([
"update": ["channel": "beta"],
"browser": ["enabled": true],
"gateway": ["mode": "local"],
"channels": [
"discord": [
"enabled": true,
"dmPolicy": "pairing",
],
],
])
_ = OpenClawConfigFile.loadDict()
#expect(!FileManager().fileExists(atPath: configHealthPath.path))
let clobbered = """
{
"update": {
"channel": "beta"
}
}
"""
try clobbered.write(to: configPath, atomically: true, encoding: .utf8)
let loaded = OpenClawConfigFile.loadDict()
#expect((loaded["gateway"] as? [String: Any]) == nil)
#expect(!FileManager().fileExists(atPath: configHealthPath.path))
let rawAudit = try String(contentsOf: auditPath, encoding: .utf8)
let lines = rawAudit
.split(whereSeparator: \.isNewline)
.map(String.init)
let observeLine = lines.reversed().first { $0.contains("\"event\":\"config.observe\"") }
#expect(observeLine != nil)
guard let observeLine else {
Issue.record("Missing config.observe audit line")
return
}
let auditRoot = try JSONSerialization.jsonObject(with: Data(observeLine.utf8)) as? [String: Any]
#expect(auditRoot?["source"] as? String == "macos-openclaw-config-file")
#expect(auditRoot?["configPath"] as? String == configPath.path)
#expect(auditRoot?["mode"] is NSNumber)
#expect(auditRoot?["ino"] as? String != nil)
#expect(auditRoot?["lastKnownGoodMode"] is NSNumber)
#expect(auditRoot?["backupMode"] is NSNull)
let suspicious = auditRoot?["suspicious"] as? [String] ?? []
#expect(suspicious.contains("gateway-mode-missing-vs-last-good"))
#expect(suspicious.contains("update-channel-only-root"))
let clobberedPath = auditRoot?["clobberedPath"] as? String
#expect(clobberedPath != nil)
if let clobberedPath {
let preserved = try String(contentsOfFile: clobberedPath, encoding: .utf8)
#expect(preserved == clobbered)
}
}
}
}
@MainActor
@Test
func `save dict records preserved gateway auth in audit`() async throws {
let stateDir = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
let configPath = stateDir.appendingPathComponent("openclaw.json")
let auditPath = stateDir.appendingPathComponent("logs/config-audit.jsonl")
defer { try? FileManager().removeItem(at: stateDir) }
try await TestIsolation.withEnvValues([
"OPENCLAW_STATE_DIR": stateDir.path,
"OPENCLAW_CONFIG_PATH": configPath.path,
]) {
OpenClawConfigFile.saveDict([
"gateway": [
"mode": "local",
"auth": [
"mode": "token",
"token": "test-token", // pragma: allowlist secret
],
],
])
let saved = OpenClawConfigFile.saveDict([
"gateway": [
"mode": "local",
],
"browser": [
"enabled": false,
],
])
#expect(saved)
let data = try Data(contentsOf: configPath)
let root = try JSONSerialization.jsonObject(with: data) as? [String: Any]
let gateway = root?["gateway"] as? [String: Any]
let auth = gateway?["auth"] as? [String: Any]
#expect(gateway?["mode"] as? String == "local")
#expect(auth?["mode"] as? String == "token")
#expect(auth?["token"] as? String == "test-token") // pragma: allowlist secret
#expect((root?["meta"] as? [String: Any]) != nil)
let rawAudit = try String(contentsOf: auditPath, encoding: .utf8)
let last = rawAudit.split(whereSeparator: \.isNewline).map(String.init).last
let auditRoot = try JSONSerialization.jsonObject(with: Data((last ?? "{}").utf8)) as? [String: Any]
#expect(auditRoot?["result"] as? String == "success")
#expect(auditRoot?["preservedGatewayAuth"] as? Bool == true)
let suspicious = auditRoot?["suspicious"] as? [String] ?? []
#expect(suspicious.contains("gateway-auth-preserved"))
}
}
@MainActor
@Test
func `save dict rejects gateway mode removal and keeps previous config`() async throws {
let stateDir = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
let configPath = stateDir.appendingPathComponent("openclaw.json")
let auditPath = stateDir.appendingPathComponent("logs/config-audit.jsonl")
defer { try? FileManager().removeItem(at: stateDir) }
try await TestIsolation.withEnvValues([
"OPENCLAW_STATE_DIR": stateDir.path,
"OPENCLAW_CONFIG_PATH": configPath.path,
]) {
OpenClawConfigFile.saveDict([
"gateway": [
"mode": "local",
"auth": [
"mode": "token",
"token": "test-token", // pragma: allowlist secret
],
],
"browser": [
"enabled": true,
],
])
let before = try String(contentsOf: configPath, encoding: .utf8)
let saved = OpenClawConfigFile.saveDict([
"browser": [
"enabled": false,
],
])
#expect(!saved)
let after = try String(contentsOf: configPath, encoding: .utf8)
#expect(after == before)
let rawAudit = try String(contentsOf: auditPath, encoding: .utf8)
let lines = rawAudit.split(whereSeparator: \.isNewline).map(String.init)
guard let last = lines.last else {
Issue.record("Missing rejected config audit line")
return
}
let auditRoot = try JSONSerialization.jsonObject(with: Data(last.utf8)) as? [String: Any]
#expect(auditRoot?["result"] as? String == "rejected")
let suspicious = auditRoot?["suspicious"] as? [String] ?? []
let blocking = auditRoot?["blocking"] as? [String] ?? []
#expect(suspicious.contains("gateway-mode-removed"))
#expect(blocking.contains("gateway-mode-removed"))
if let rejectedPath = auditRoot?["rejectedPath"] as? String {
#expect(FileManager().fileExists(atPath: rejectedPath))
let attributes = try FileManager().attributesOfItem(atPath: rejectedPath)
let mode = attributes[.posixPermissions] as? NSNumber
#expect(mode?.intValue == 0o600)
} else {
Issue.record("Missing rejected payload path")
}
}
}
}