Files
openclaw/apps/macos/Tests/OpenClawIPCTests/TailscaleIntegrationSectionTests.swift
Val Alexander da982a3118 fix(macos): avoid Tailscale hydration config rewrites
Fixes #59545.

Suppress the macOS General/Tailscale initial hydration apply path from rewriting openclaw.json when settings are unchanged, and add regression coverage for gateway/auth/meta/wizard preservation.

Verified on the retry head 8a30aa831c:
- GitHub CI completed successfully, including macos-node, macos-swift, check-docs, security, Workflow Sanity, and OpenGrep.
- Review threads were empty before merge.
- Duplicate sweep kept #59545 as the canonical standalone issue; no duplicate closures were appropriate.
2026-05-02 03:23:00 -05:00

132 lines
4.9 KiB
Swift

import SwiftUI
import Testing
@testable import OpenClaw
@Suite(.serialized)
@MainActor
struct TailscaleIntegrationSectionTests {
@Test func `tailscale section builds body when not installed`() {
let service = TailscaleService(isInstalled: false, isRunning: false, statusError: "not installed")
var view = TailscaleIntegrationSection(connectionMode: .local, isPaused: false)
view.setTestingService(service)
view.setTestingState(mode: "off", requireCredentials: false, statusMessage: "Idle")
_ = view.body
}
@Test func `tailscale section builds body for serve mode`() {
let service = TailscaleService(
isInstalled: true,
isRunning: true,
tailscaleHostname: "openclaw.tailnet.ts.net",
tailscaleIP: "100.64.0.1")
var view = TailscaleIntegrationSection(connectionMode: .local, isPaused: false)
view.setTestingService(service)
view.setTestingState(
mode: "serve",
requireCredentials: true,
password: "secret",
statusMessage: "Running")
_ = view.body
}
@Test func `tailscale section builds body for funnel mode`() {
let service = TailscaleService(
isInstalled: true,
isRunning: false,
tailscaleHostname: nil,
tailscaleIP: nil,
statusError: "not running")
var view = TailscaleIntegrationSection(connectionMode: .remote, isPaused: false)
view.setTestingService(service)
view.setTestingState(
mode: "funnel",
requireCredentials: false,
statusMessage: "Needs start",
validationMessage: "Invalid token")
_ = view.body
}
@Test func `general tailscale hydration does not rewrite existing 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) }
try FileManager().createDirectory(at: stateDir, withIntermediateDirectories: true)
let initialConfig = """
{
"meta": {
"lastTouchedVersion": "2026.3.28",
"lastTouchedAt": "2026-03-31T13:15:24.532Z"
},
"wizard": {
"lastRunAt": "2026-03-30T14:24:54.570Z",
"lastRunVersion": "2026.3.24"
},
"gateway": {
"mode": "local",
"port": 18789,
"bind": "auto",
"tailscale": {
"mode": "serve"
},
"auth": {
"mode": "token",
"token": "existing-token"
}
}
}
"""
try initialConfig.write(to: configPath, atomically: true, encoding: .utf8)
try await TestIsolation.withEnvValues([
"OPENCLAW_STATE_DIR": stateDir.path,
"OPENCLAW_CONFIG_PATH": configPath.path,
]) {
let before = try Data(contentsOf: configPath)
let root = try #require(
JSONSerialization.jsonObject(with: before) as? [String: Any])
await TailscaleIntegrationSection.simulateHydrationApplyForTesting(
root: root,
connectionMode: .local,
isPaused: true,
saveRoot: { root in
OpenClawConfigFile.saveDict(root, allowGatewayAuthMutation: true)
})
let after = try Data(contentsOf: configPath)
#expect(after == before)
let afterRoot = try #require(
JSONSerialization.jsonObject(with: after) as? [String: Any])
let gateway = try #require(afterRoot["gateway"] as? [String: Any])
let auth = try #require(gateway["auth"] as? [String: Any])
let meta = try #require(afterRoot["meta"] as? [String: Any])
let wizard = try #require(afterRoot["wizard"] as? [String: Any])
#expect(gateway["bind"] as? String == "auto")
#expect(auth["mode"] as? String == "token")
#expect(auth["token"] as? String == "existing-token") // pragma: allowlist secret
#expect(meta["lastTouchedAt"] as? String == "2026-03-31T13:15:24.532Z")
#expect(wizard["lastRunAt"] as? String == "2026-03-30T14:24:54.570Z")
#expect(wizard["lastRunVersion"] as? String == "2026.3.24")
}
}
@Test func `unchanged tailscale apply clears stale messages`() {
let messages = TailscaleIntegrationSection.messagesForTesting(
didApply: false,
success: true,
connectionMode: .local,
isPaused: false)
#expect(messages.statusMessage == nil)
#expect(messages.validationMessage == nil)
#expect(messages.shouldRecordSuccess == false)
#expect(messages.shouldRestartGateway == false)
}
}