Files
openclaw/apps/macos/Tests/OpenClawIPCTests/TestIsolation.swift
Peter Steinberger 0dbdf994b3 feat(macos): isolate named app profiles (#121136)
* feat(macos): isolate named app profiles

* refactor(macos): isolate profile launch ownership

* fix(macos): avoid overlapping approvals socket access

* fix(macos): declare profile defaults concurrency ownership

* fix(macos): return profiled node launch arguments

* chore(i18n): refresh macOS profile source inventory

* fix(macos): gate profile startup before services

* test(macos): evaluate profile state before assertions

* fix(daemon): skip absent launchd deactivation

* fix(macos): fail closed on profile port conflicts

* chore(i18n): refresh profile conflict inventory

* fix(macos): ignore non-gateway launch agent claims

* test(macos): stabilize profile lifecycle timing

* fix(macos): remove stale dashboard URL

* chore(macos): refresh native source baseline
2026-08-09 14:50:15 -07:00

136 lines
4.1 KiB
Swift

import Foundation
import Testing
@testable import OpenClaw
@testable import OpenClawKit
actor TestIsolationLock {
static let shared = TestIsolationLock()
private var locked = false
private var waiters: [CheckedContinuation<Void, Never>] = []
func acquire() async {
if !self.locked {
self.locked = true
return
}
await withCheckedContinuation { cont in
self.waiters.append(cont)
}
// `unlock()` resumed us; lock is now held for this caller.
}
func release() {
if self.waiters.isEmpty {
self.locked = false
return
}
let next = self.waiters.removeFirst()
next.resume()
}
}
@MainActor
enum TestIsolation {
static func withIsolatedState<T>(
env: [String: String?] = [:],
defaults: [String: Any?] = [:],
_ body: () async throws -> T) async rethrows -> T
{
func restoreUserDefaults(_ values: [String: Any?], userDefaults: UserDefaults) {
for (key, value) in values {
if let value {
userDefaults.set(value, forKey: key)
} else {
userDefaults.removeObject(forKey: key)
}
}
}
func restoreEnv(_ values: [String: String?]) {
for (key, value) in values {
if let value {
setenv(key, value, 1)
} else {
unsetenv(key)
}
}
}
await TestIsolationLock.shared.acquire()
var previousEnv: [String: String?] = [:]
for (key, value) in env {
previousEnv[key] = getenv(key).map { String(cString: $0) }
if let value {
setenv(key, value, 1)
} else {
unsetenv(key)
}
}
let userDefaults = AppDefaults.standard
var previousDefaults: [String: Any?] = [:]
for (key, value) in defaults {
previousDefaults[key] = userDefaults.object(forKey: key)
if let value {
userDefaults.set(value, forKey: key)
} else {
userDefaults.removeObject(forKey: key)
}
}
do {
let result = try await body()
restoreUserDefaults(previousDefaults, userDefaults: userDefaults)
restoreEnv(previousEnv)
await TestIsolationLock.shared.release()
return result
} catch {
restoreUserDefaults(previousDefaults, userDefaults: userDefaults)
restoreEnv(previousEnv)
await TestIsolationLock.shared.release()
throw error
}
}
static func withEnvValues<T>(
_ values: [String: String?],
_ body: () async throws -> T) async rethrows -> T
{
try await self.withIsolatedState(env: values, defaults: [:], body)
}
static func withUserDefaultsValues<T>(
_ values: [String: Any?],
_ body: () async throws -> T) async rethrows -> T
{
try await self.withIsolatedState(env: [:], defaults: values, body)
}
nonisolated static func tempConfigPath() -> String {
FileManager().temporaryDirectory
.appendingPathComponent("openclaw-test-config-\(UUID().uuidString).json")
.path
}
}
struct ExecApprovalsStateIsolationTrait: TestTrait, TestScoping {
func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: @Sendable () async throws -> Void) async throws
{
let stateDirectory = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-approvals-state-\(UUID().uuidString)", isDirectory: true)
try FileManager().createDirectory(at: stateDirectory, withIntermediateDirectories: true)
defer { try? FileManager().removeItem(at: stateDirectory) }
try await ExecApprovalsStore.withStateDirectory(stateDirectory, operation: function)
}
}
extension Trait where Self == ExecApprovalsStateIsolationTrait {
static var execApprovalsStateIsolated: Self {
Self()
}
}