Files
openclaw/apps/shared/OpenClawKit/Tests/OpenClawKitTests/StateDirectoryIsolationTrait.swift
Peter Steinberger b5f67ac4c7 fix(shared): skip app-group identity migration when OPENCLAW_STATE_DIR is overridden (#101779)
* fix(shared): skip app-group identity migration when OPENCLAW_STATE_DIR is overridden

An unentitled process (mac app, swift test runners) with a fresh state dir
imported the machine's real app-group device identity and auth tokens into
any explicitly overridden state dir, violating the documented legacy-fallback
contract. On developer Macs this leaked real operator/node device tokens into
per-test temp state dirs, breaking the nil-token expectations in
GatewayNodeSessionTests (non bootstrap hello / untrusted bootstrap hello)
for local swift test runs while clean CI passed.

Migration now requires no OPENCLAW_STATE_DIR override, with a regression
test. StateDirectoryIsolationTrait owns per-test state-dir pinning (fresh
temp dir per gated test, env restored to launch value or a run-scoped
quarantine dir, never unset), replacing the hand-rolled setenv/defer
boilerplate in every gated test.

* test(shared): remove redundant throws annotations
2026-07-07 18:48:21 +01:00

89 lines
3.4 KiB
Swift

import Foundation
import Testing
private actor StateDirectoryTestGate {
private var locked = false
private var waiters: [CheckedContinuation<Void, Never>] = []
func acquire() async {
if !self.locked {
self.locked = true
return
}
await withCheckedContinuation { continuation in
self.waiters.append(continuation)
}
}
func release() {
guard !self.waiters.isEmpty else {
self.locked = false
return
}
self.waiters.removeFirst().resume()
}
}
/// Serializes state-directory tests and pins `OPENCLAW_STATE_DIR` to a fresh
/// per-test temp dir. Device identity/auth stores resolve that env var on every
/// call; without the pin they fall back to the developer's real store
/// (app group container or ~/Library/Application Support/OpenClaw), so real
/// device tokens leak into nil-token assertions on machines that ran the app.
/// Tests needing a custom dir (for example an unwritable path) may still set
/// the env var themselves and restore the previous value.
struct StateDirectoryIsolationTrait: TestTrait, TestScoping {
private static let gate = StateDirectoryTestGate()
/// Value restored between gated tests: the operator-provided launch value
/// when present, else a run-scoped quarantine dir. Never restore to unset
/// leaked background tasks (reconnect watchdogs from failed tests) would
/// then write fake tokens into the developer's real store.
private static let restoreStateDirPath: String = {
if let raw = getenv("OPENCLAW_STATE_DIR") {
let value = String(cString: raw).trimmingCharacters(in: .whitespacesAndNewlines)
if !value.isEmpty {
return value
}
}
let quarantine = FileManager.default.temporaryDirectory
.appendingPathComponent("openclaw-test-state-quarantine-\(UUID().uuidString)", isDirectory: true)
try? FileManager.default.createDirectory(at: quarantine, withIntermediateDirectories: true)
return quarantine.path
}()
func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: @Sendable () async throws -> Void) async throws
{
await Self.gate.acquire()
// Resolve the restore value before the first setenv so it captures the
// launch environment, not a prior test's temp dir.
let restorePath = Self.restoreStateDirPath
let stateDir = FileManager.default.temporaryDirectory
.appendingPathComponent("openclaw-test-state-\(UUID().uuidString)", isDirectory: true)
var thrown: (any Error)?
do {
try FileManager.default.createDirectory(at: stateDir, withIntermediateDirectories: true)
setenv("OPENCLAW_STATE_DIR", stateDir.path, 1)
try await function()
} catch {
thrown = error
}
// Restore env before releasing the gate; the next gated test sets its
// own dir immediately after acquire and must not be clobbered.
setenv("OPENCLAW_STATE_DIR", restorePath, 1)
try? FileManager.default.removeItem(at: stateDir)
await Self.gate.release()
if let thrown {
throw thrown
}
}
}
extension Trait where Self == StateDirectoryIsolationTrait {
static var stateDirectoryIsolated: Self {
Self()
}
}