From 2b6adc9e60a159c8a061499ec8b47dfef84e6736 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 7 Jan 2026 20:30:57 +0000 Subject: [PATCH] test(macos): make env/defaults helper Swift 6-safe --- .../LowCoverageHelperTests.swift | 2 +- .../ClawdbotIPCTests/TestIsolation.swift | 102 ++++++++++-------- 2 files changed, 58 insertions(+), 46 deletions(-) diff --git a/apps/macos/Tests/ClawdbotIPCTests/LowCoverageHelperTests.swift b/apps/macos/Tests/ClawdbotIPCTests/LowCoverageHelperTests.swift index a22bdab44..6ee7cc012 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/LowCoverageHelperTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/LowCoverageHelperTests.swift @@ -94,7 +94,7 @@ struct LowCoverageHelperTests { } @Test func gatewayLaunchAgentHelpers() async throws { - try await TestIsolation.withEnvValues( + await TestIsolation.withEnvValues( [ "CLAWDBOT_GATEWAY_BIND": "Lan", "CLAWDBOT_GATEWAY_TOKEN": " secret ", diff --git a/apps/macos/Tests/ClawdbotIPCTests/TestIsolation.swift b/apps/macos/Tests/ClawdbotIPCTests/TestIsolation.swift index 0613c830c..03c32607f 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/TestIsolation.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/TestIsolation.swift @@ -6,7 +6,7 @@ actor TestIsolationLock { private var locked = false private var waiters: [CheckedContinuation] = [] - private func lock() async { + func acquire() async { if !self.locked { self.locked = true return @@ -17,7 +17,7 @@ actor TestIsolationLock { // `unlock()` resumed us; lock is now held for this caller. } - private func unlock() { + func release() { if self.waiters.isEmpty { self.locked = false return @@ -25,78 +25,90 @@ actor TestIsolationLock { let next = self.waiters.removeFirst() next.resume() } - - func withLock(_ body: @Sendable () async throws -> T) async rethrows -> T { - await self.lock() - defer { self.unlock() } - return try await body() - } } +@MainActor enum TestIsolation { - static func withIsolatedState( + static func withIsolatedState( env: [String: String?] = [:], defaults: [String: Any?] = [:], - _ body: @Sendable () async throws -> T) async rethrows -> T + _ body: () async throws -> T) async rethrows -> T { - try await TestIsolationLock.shared.withLock { - 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) - } + 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 = UserDefaults.standard - var previousDefaults: [String: Any?] = [:] - for (key, value) in defaults { - previousDefaults[key] = userDefaults.object(forKey: key) + let userDefaults = UserDefaults.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() + for (key, value) in previousDefaults { if let value { userDefaults.set(value, forKey: key) } else { userDefaults.removeObject(forKey: key) } } - - defer { - for (key, value) in previousDefaults { - if let value { - userDefaults.set(value, forKey: key) - } else { - userDefaults.removeObject(forKey: key) - } - } - for (key, value) in previousEnv { - if let value { - setenv(key, value, 1) - } else { - unsetenv(key) - } + for (key, value) in previousEnv { + if let value { + setenv(key, value, 1) + } else { + unsetenv(key) } } - - return try await body() + await TestIsolationLock.shared.release() + return result + } catch { + for (key, value) in previousDefaults { + if let value { + userDefaults.set(value, forKey: key) + } else { + userDefaults.removeObject(forKey: key) + } + } + for (key, value) in previousEnv { + if let value { + setenv(key, value, 1) + } else { + unsetenv(key) + } + } + await TestIsolationLock.shared.release() + throw error } } - static func withEnvValues( + static func withEnvValues( _ values: [String: String?], - _ body: @Sendable () async throws -> T) async rethrows -> T + _ body: () async throws -> T) async rethrows -> T { try await Self.withIsolatedState(env: values, defaults: [:], body) } - static func withUserDefaultsValues( + static func withUserDefaultsValues( _ values: [String: Any?], - _ body: @Sendable () async throws -> T) async rethrows -> T + _ body: () async throws -> T) async rethrows -> T { try await Self.withIsolatedState(env: [:], defaults: values, body) } - static func tempConfigPath() -> String { + nonisolated static func tempConfigPath() -> String { FileManager.default.temporaryDirectory .appendingPathComponent("clawdbot-test-config-\(UUID().uuidString).json") .path