From fc692d82fd1c4a6d8763686fe66ed7bde5849bdb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Mar 2026 09:55:34 +0000 Subject: [PATCH] refactor(tests): dedupe macos ipc smoke setup blocks --- .../ChannelsSettingsSmokeTests.swift | 75 +++++++++---------- .../CommandResolverTests.swift | 44 ++++++----- .../CronJobEditorSmokeTests.swift | 45 ++++------- .../OpenClawIPCTests/CronModelsTests.swift | 53 +++++++------ .../OpenClawIPCTests/ExecAllowlistTests.swift | 28 +++---- .../ExecApprovalsStoreRefactorTests.swift | 37 +++++---- .../GatewayChannelConfigureTests.swift | 42 +++++------ .../GatewayDiscoveryHelpersTests.swift | 32 ++++---- .../GatewayEndpointStoreTests.swift | 43 +++++------ .../GatewayWebSocketTestSupport.swift | 26 +++---- .../OpenClawConfigFileTests.swift | 25 +++---- .../SkillsSettingsSmokeTests.swift | 74 ++++++++++-------- .../OpenClawIPCTests/TestIsolation.swift | 52 ++++++------- .../VoiceWakeGlobalSettingsSyncTests.swift | 39 +++++----- 14 files changed, 280 insertions(+), 335 deletions(-) diff --git a/apps/macos/Tests/OpenClawIPCTests/ChannelsSettingsSmokeTests.swift b/apps/macos/Tests/OpenClawIPCTests/ChannelsSettingsSmokeTests.swift index 8810d12385b..ef760472901 100644 --- a/apps/macos/Tests/OpenClawIPCTests/ChannelsSettingsSmokeTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/ChannelsSettingsSmokeTests.swift @@ -5,23 +5,44 @@ import Testing private typealias SnapshotAnyCodable = OpenClaw.AnyCodable +private let channelOrder = ["whatsapp", "telegram", "signal", "imessage"] +private let channelLabels = [ + "whatsapp": "WhatsApp", + "telegram": "Telegram", + "signal": "Signal", + "imessage": "iMessage", +] +private let channelDefaultAccountId = [ + "whatsapp": "default", + "telegram": "default", + "signal": "default", + "imessage": "default", +] + +@MainActor +private func makeChannelsStore( + channels: [String: SnapshotAnyCodable], + ts: Double = 1_700_000_000_000) -> ChannelsStore +{ + let store = ChannelsStore(isPreview: true) + store.snapshot = ChannelsStatusSnapshot( + ts: ts, + channelOrder: channelOrder, + channelLabels: channelLabels, + channelDetailLabels: nil, + channelSystemImages: nil, + channelMeta: nil, + channels: channels, + channelAccounts: [:], + channelDefaultAccountId: channelDefaultAccountId) + return store +} + @Suite(.serialized) @MainActor struct ChannelsSettingsSmokeTests { @Test func channelsSettingsBuildsBodyWithSnapshot() { - let store = ChannelsStore(isPreview: true) - store.snapshot = ChannelsStatusSnapshot( - ts: 1_700_000_000_000, - channelOrder: ["whatsapp", "telegram", "signal", "imessage"], - channelLabels: [ - "whatsapp": "WhatsApp", - "telegram": "Telegram", - "signal": "Signal", - "imessage": "iMessage", - ], - channelDetailLabels: nil, - channelSystemImages: nil, - channelMeta: nil, + let store = makeChannelsStore( channels: [ "whatsapp": SnapshotAnyCodable([ "configured": true, @@ -77,13 +98,6 @@ struct ChannelsSettingsSmokeTests { "probe": ["ok": false, "error": "imsg not found (imsg)"], "lastProbeAt": 1_700_000_050_000, ]), - ], - channelAccounts: [:], - channelDefaultAccountId: [ - "whatsapp": "default", - "telegram": "default", - "signal": "default", - "imessage": "default", ]) store.whatsappLoginMessage = "Scan QR" @@ -95,19 +109,7 @@ struct ChannelsSettingsSmokeTests { } @Test func channelsSettingsBuildsBodyWithoutSnapshot() { - let store = ChannelsStore(isPreview: true) - store.snapshot = ChannelsStatusSnapshot( - ts: 1_700_000_000_000, - channelOrder: ["whatsapp", "telegram", "signal", "imessage"], - channelLabels: [ - "whatsapp": "WhatsApp", - "telegram": "Telegram", - "signal": "Signal", - "imessage": "iMessage", - ], - channelDetailLabels: nil, - channelSystemImages: nil, - channelMeta: nil, + let store = makeChannelsStore( channels: [ "whatsapp": SnapshotAnyCodable([ "configured": false, @@ -149,13 +151,6 @@ struct ChannelsSettingsSmokeTests { "probe": ["ok": false, "error": "imsg not found (imsg)"], "lastProbeAt": 1_700_000_200_000, ]), - ], - channelAccounts: [:], - channelDefaultAccountId: [ - "whatsapp": "default", - "telegram": "default", - "signal": "default", - "imessage": "default", ]) let view = ChannelsSettings(store: store) diff --git a/apps/macos/Tests/OpenClawIPCTests/CommandResolverTests.swift b/apps/macos/Tests/OpenClawIPCTests/CommandResolverTests.swift index 6cd22f7e031..89fffd9dabf 100644 --- a/apps/macos/Tests/OpenClawIPCTests/CommandResolverTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/CommandResolverTests.swift @@ -9,9 +9,22 @@ import Testing UserDefaults(suiteName: "CommandResolverTests.\(UUID().uuidString)")! } - @Test func prefersOpenClawBinary() throws { + private func makeLocalDefaults() -> UserDefaults { let defaults = self.makeDefaults() defaults.set(AppState.ConnectionMode.local.rawValue, forKey: connectionModeKey) + return defaults + } + + private func makeProjectRootWithPnpm() throws -> (tmp: URL, pnpmPath: URL) { + let tmp = try makeTempDirForTests() + CommandResolver.setProjectRoot(tmp.path) + let pnpmPath = tmp.appendingPathComponent("node_modules/.bin/pnpm") + try makeExecutableForTests(at: pnpmPath) + return (tmp, pnpmPath) + } + + @Test func prefersOpenClawBinary() throws { + let defaults = self.makeLocalDefaults() let tmp = try makeTempDirForTests() CommandResolver.setProjectRoot(tmp.path) @@ -24,8 +37,7 @@ import Testing } @Test func fallsBackToNodeAndScript() throws { - let defaults = self.makeDefaults() - defaults.set(AppState.ConnectionMode.local.rawValue, forKey: connectionModeKey) + let defaults = self.makeLocalDefaults() let tmp = try makeTempDirForTests() CommandResolver.setProjectRoot(tmp.path) @@ -52,8 +64,7 @@ import Testing } @Test func prefersOpenClawBinaryOverPnpm() throws { - let defaults = self.makeDefaults() - defaults.set(AppState.ConnectionMode.local.rawValue, forKey: connectionModeKey) + let defaults = self.makeLocalDefaults() let tmp = try makeTempDirForTests() CommandResolver.setProjectRoot(tmp.path) @@ -74,8 +85,7 @@ import Testing } @Test func usesOpenClawBinaryWithoutNodeRuntime() throws { - let defaults = self.makeDefaults() - defaults.set(AppState.ConnectionMode.local.rawValue, forKey: connectionModeKey) + let defaults = self.makeLocalDefaults() let tmp = try makeTempDirForTests() CommandResolver.setProjectRoot(tmp.path) @@ -94,14 +104,8 @@ import Testing } @Test func fallsBackToPnpm() throws { - let defaults = self.makeDefaults() - defaults.set(AppState.ConnectionMode.local.rawValue, forKey: connectionModeKey) - - let tmp = try makeTempDirForTests() - CommandResolver.setProjectRoot(tmp.path) - - let pnpmPath = tmp.appendingPathComponent("node_modules/.bin/pnpm") - try makeExecutableForTests(at: pnpmPath) + let defaults = self.makeLocalDefaults() + let (tmp, pnpmPath) = try self.makeProjectRootWithPnpm() let cmd = CommandResolver.openclawCommand( subcommand: "rpc", @@ -113,14 +117,8 @@ import Testing } @Test func pnpmKeepsExtraArgsAfterSubcommand() throws { - let defaults = self.makeDefaults() - defaults.set(AppState.ConnectionMode.local.rawValue, forKey: connectionModeKey) - - let tmp = try makeTempDirForTests() - CommandResolver.setProjectRoot(tmp.path) - - let pnpmPath = tmp.appendingPathComponent("node_modules/.bin/pnpm") - try makeExecutableForTests(at: pnpmPath) + let defaults = self.makeLocalDefaults() + let (tmp, pnpmPath) = try self.makeProjectRootWithPnpm() let cmd = CommandResolver.openclawCommand( subcommand: "health", diff --git a/apps/macos/Tests/OpenClawIPCTests/CronJobEditorSmokeTests.swift b/apps/macos/Tests/OpenClawIPCTests/CronJobEditorSmokeTests.swift index 210e3e63bab..d0304f070b1 100644 --- a/apps/macos/Tests/OpenClawIPCTests/CronJobEditorSmokeTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/CronJobEditorSmokeTests.swift @@ -5,20 +5,23 @@ import Testing @Suite(.serialized) @MainActor struct CronJobEditorSmokeTests { + private func makeEditor(job: CronJob? = nil, channelsStore: ChannelsStore? = nil) -> CronJobEditor { + CronJobEditor( + job: job, + isSaving: .constant(false), + error: .constant(nil), + channelsStore: channelsStore ?? ChannelsStore(isPreview: true), + onCancel: {}, + onSave: { _ in }) + } + @Test func statusPillBuildsBody() { _ = StatusPill(text: "ok", tint: .green).body _ = StatusPill(text: "disabled", tint: .secondary).body } @Test func cronJobEditorBuildsBodyForNewJob() { - let channelsStore = ChannelsStore(isPreview: true) - let view = CronJobEditor( - job: nil, - isSaving: .constant(false), - error: .constant(nil), - channelsStore: channelsStore, - onCancel: {}, - onSave: { _ in }) + let view = self.makeEditor() _ = view.body } @@ -53,37 +56,17 @@ struct CronJobEditorSmokeTests { lastError: nil, lastDurationMs: 1000)) - let view = CronJobEditor( - job: job, - isSaving: .constant(false), - error: .constant(nil), - channelsStore: channelsStore, - onCancel: {}, - onSave: { _ in }) + let view = self.makeEditor(job: job, channelsStore: channelsStore) _ = view.body } @Test func cronJobEditorExercisesBuilders() { - let channelsStore = ChannelsStore(isPreview: true) - var view = CronJobEditor( - job: nil, - isSaving: .constant(false), - error: .constant(nil), - channelsStore: channelsStore, - onCancel: {}, - onSave: { _ in }) + var view = self.makeEditor() view.exerciseForTesting() } @Test func cronJobEditorIncludesDeleteAfterRunForAtSchedule() { - let channelsStore = ChannelsStore(isPreview: true) - let view = CronJobEditor( - job: nil, - isSaving: .constant(false), - error: .constant(nil), - channelsStore: channelsStore, - onCancel: {}, - onSave: { _ in }) + let view = self.makeEditor() var root: [String: Any] = [:] view.applyDeleteAfterRun(to: &root, scheduleKind: CronJobEditor.ScheduleKind.at, deleteAfterRun: true) diff --git a/apps/macos/Tests/OpenClawIPCTests/CronModelsTests.swift b/apps/macos/Tests/OpenClawIPCTests/CronModelsTests.swift index f90ac25a9d7..c7e15184351 100644 --- a/apps/macos/Tests/OpenClawIPCTests/CronModelsTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/CronModelsTests.swift @@ -4,6 +4,28 @@ import Testing @Suite struct CronModelsTests { + private func makeCronJob( + name: String, + payloadText: String, + state: CronJobState = CronJobState()) -> CronJob + { + CronJob( + id: "x", + agentId: nil, + name: name, + description: nil, + enabled: true, + deleteAfterRun: nil, + createdAtMs: 0, + updatedAtMs: 0, + schedule: .at(at: "2026-02-03T18:00:00Z"), + sessionTarget: .main, + wakeMode: .now, + payload: .systemEvent(text: payloadText), + delivery: nil, + state: state) + } + @Test func scheduleAtEncodesAndDecodes() throws { let schedule = CronSchedule.at(at: "2026-02-03T18:00:00Z") let data = try JSONEncoder().encode(schedule) @@ -91,21 +113,7 @@ struct CronModelsTests { } @Test func displayNameTrimsWhitespaceAndFallsBack() { - let base = CronJob( - id: "x", - agentId: nil, - name: " hello ", - description: nil, - enabled: true, - deleteAfterRun: nil, - createdAtMs: 0, - updatedAtMs: 0, - schedule: .at(at: "2026-02-03T18:00:00Z"), - sessionTarget: .main, - wakeMode: .now, - payload: .systemEvent(text: "hi"), - delivery: nil, - state: CronJobState()) + let base = makeCronJob(name: " hello ", payloadText: "hi") #expect(base.displayName == "hello") var unnamed = base @@ -114,20 +122,9 @@ struct CronModelsTests { } @Test func nextRunDateAndLastRunDateDeriveFromState() { - let job = CronJob( - id: "x", - agentId: nil, + let job = makeCronJob( name: "t", - description: nil, - enabled: true, - deleteAfterRun: nil, - createdAtMs: 0, - updatedAtMs: 0, - schedule: .at(at: "2026-02-03T18:00:00Z"), - sessionTarget: .main, - wakeMode: .now, - payload: .systemEvent(text: "hi"), - delivery: nil, + payloadText: "hi", state: CronJobState( nextRunAtMs: 1_700_000_000_000, runningAtMs: nil, diff --git a/apps/macos/Tests/OpenClawIPCTests/ExecAllowlistTests.swift b/apps/macos/Tests/OpenClawIPCTests/ExecAllowlistTests.swift index b63533177b5..71d979be96f 100644 --- a/apps/macos/Tests/OpenClawIPCTests/ExecAllowlistTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/ExecAllowlistTests.swift @@ -51,24 +51,24 @@ struct ExecAllowlistTests { .appendingPathComponent(filename) } - @Test func matchUsesResolvedPath() { - let entry = ExecAllowlistEntry(pattern: "/opt/homebrew/bin/rg") - let resolution = ExecCommandResolution( + private static func homebrewRGResolution() -> ExecCommandResolution { + ExecCommandResolution( rawExecutable: "rg", resolvedPath: "/opt/homebrew/bin/rg", executableName: "rg", cwd: nil) + } + + @Test func matchUsesResolvedPath() { + let entry = ExecAllowlistEntry(pattern: "/opt/homebrew/bin/rg") + let resolution = Self.homebrewRGResolution() let match = ExecAllowlistMatcher.match(entries: [entry], resolution: resolution) #expect(match?.pattern == entry.pattern) } @Test func matchIgnoresBasenamePattern() { let entry = ExecAllowlistEntry(pattern: "rg") - let resolution = ExecCommandResolution( - rawExecutable: "rg", - resolvedPath: "/opt/homebrew/bin/rg", - executableName: "rg", - cwd: nil) + let resolution = Self.homebrewRGResolution() let match = ExecAllowlistMatcher.match(entries: [entry], resolution: resolution) #expect(match == nil) } @@ -86,22 +86,14 @@ struct ExecAllowlistTests { @Test func matchIsCaseInsensitive() { let entry = ExecAllowlistEntry(pattern: "/OPT/HOMEBREW/BIN/RG") - let resolution = ExecCommandResolution( - rawExecutable: "rg", - resolvedPath: "/opt/homebrew/bin/rg", - executableName: "rg", - cwd: nil) + let resolution = Self.homebrewRGResolution() let match = ExecAllowlistMatcher.match(entries: [entry], resolution: resolution) #expect(match?.pattern == entry.pattern) } @Test func matchSupportsGlobStar() { let entry = ExecAllowlistEntry(pattern: "/opt/**/rg") - let resolution = ExecCommandResolution( - rawExecutable: "rg", - resolvedPath: "/opt/homebrew/bin/rg", - executableName: "rg", - cwd: nil) + let resolution = Self.homebrewRGResolution() let match = ExecAllowlistMatcher.match(entries: [entry], resolution: resolution) #expect(match?.pattern == entry.pattern) } diff --git a/apps/macos/Tests/OpenClawIPCTests/ExecApprovalsStoreRefactorTests.swift b/apps/macos/Tests/OpenClawIPCTests/ExecApprovalsStoreRefactorTests.swift index 9337ee8c947..42dcf106d1e 100644 --- a/apps/macos/Tests/OpenClawIPCTests/ExecApprovalsStoreRefactorTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/ExecApprovalsStoreRefactorTests.swift @@ -4,13 +4,21 @@ import Testing @Suite(.serialized) struct ExecApprovalsStoreRefactorTests { - @Test - func ensureFileSkipsRewriteWhenUnchanged() async throws { + private func withTempStateDir( + _ body: @escaping @Sendable (URL) async throws -> Void) async throws + { let stateDir = FileManager().temporaryDirectory .appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true) defer { try? FileManager().removeItem(at: stateDir) } try await TestIsolation.withEnvValues(["OPENCLAW_STATE_DIR": stateDir.path]) { + try await body(stateDir) + } + } + + @Test + func ensureFileSkipsRewriteWhenUnchanged() async throws { + try await self.withTempStateDir { stateDir in _ = ExecApprovalsStore.ensureFile() let url = ExecApprovalsStore.fileURL() let firstWriteDate = try Self.modificationDate(at: url) @@ -24,12 +32,8 @@ struct ExecApprovalsStoreRefactorTests { } @Test - func updateAllowlistReportsRejectedBasenamePattern() async { - let stateDir = FileManager().temporaryDirectory - .appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true) - defer { try? FileManager().removeItem(at: stateDir) } - - await TestIsolation.withEnvValues(["OPENCLAW_STATE_DIR": stateDir.path]) { + func updateAllowlistReportsRejectedBasenamePattern() async throws { + try await self.withTempStateDir { _ in let rejected = ExecApprovalsStore.updateAllowlist( agentId: "main", allowlist: [ @@ -46,12 +50,8 @@ struct ExecApprovalsStoreRefactorTests { } @Test - func updateAllowlistMigratesLegacyPatternFromResolvedPath() async { - let stateDir = FileManager().temporaryDirectory - .appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true) - defer { try? FileManager().removeItem(at: stateDir) } - - await TestIsolation.withEnvValues(["OPENCLAW_STATE_DIR": stateDir.path]) { + func updateAllowlistMigratesLegacyPatternFromResolvedPath() async throws { + try await self.withTempStateDir { _ in let rejected = ExecApprovalsStore.updateAllowlist( agentId: "main", allowlist: [ @@ -70,13 +70,10 @@ struct ExecApprovalsStoreRefactorTests { @Test func ensureFileHardensStateDirectoryPermissions() async throws { - let stateDir = FileManager().temporaryDirectory - .appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true) - defer { try? FileManager().removeItem(at: stateDir) } - try FileManager().createDirectory(at: stateDir, withIntermediateDirectories: true) - try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: stateDir.path) + try await self.withTempStateDir { stateDir in + try FileManager().createDirectory(at: stateDir, withIntermediateDirectories: true) + try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: stateDir.path) - try await TestIsolation.withEnvValues(["OPENCLAW_STATE_DIR": stateDir.path]) { _ = ExecApprovalsStore.ensureFile() let attrs = try FileManager().attributesOfItem(atPath: stateDir.path) let permissions = (attrs[.posixPermissions] as? NSNumber)?.intValue ?? -1 diff --git a/apps/macos/Tests/OpenClawIPCTests/GatewayChannelConfigureTests.swift b/apps/macos/Tests/OpenClawIPCTests/GatewayChannelConfigureTests.swift index c6f2ffb2ff1..f1d87fdac5f 100644 --- a/apps/macos/Tests/OpenClawIPCTests/GatewayChannelConfigureTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/GatewayChannelConfigureTests.swift @@ -5,6 +5,18 @@ import Testing @testable import OpenClaw @Suite struct GatewayConnectionTests { + private func makeConnection( + session: GatewayTestWebSocketSession, + token: String? = nil) throws -> (GatewayConnection, ConfigSource) + { + let url = try #require(URL(string: "ws://example.invalid")) + let cfg = ConfigSource(token: token) + let conn = GatewayConnection( + configProvider: { (url: url, token: cfg.snapshotToken(), password: nil) }, + sessionBox: WebSocketSessionBox(session: session)) + return (conn, cfg) + } + private func makeSession(helloDelayMs: Int = 0) -> GatewayTestWebSocketSession { GatewayTestWebSocketSession( taskFactory: { @@ -46,11 +58,7 @@ import Testing @Test func requestReusesSingleWebSocketForSameConfig() async throws { let session = self.makeSession() - let url = try #require(URL(string: "ws://example.invalid")) - let cfg = ConfigSource(token: nil) - let conn = GatewayConnection( - configProvider: { (url: url, token: cfg.snapshotToken(), password: nil) }, - sessionBox: WebSocketSessionBox(session: session)) + let (conn, _) = try self.makeConnection(session: session) _ = try await conn.request(method: "status", params: nil) #expect(session.snapshotMakeCount() == 1) @@ -62,11 +70,7 @@ import Testing @Test func requestReconfiguresAndCancelsOnTokenChange() async throws { let session = self.makeSession() - let url = try #require(URL(string: "ws://example.invalid")) - let cfg = ConfigSource(token: "a") - let conn = GatewayConnection( - configProvider: { (url: url, token: cfg.snapshotToken(), password: nil) }, - sessionBox: WebSocketSessionBox(session: session)) + let (conn, cfg) = try self.makeConnection(session: session, token: "a") _ = try await conn.request(method: "status", params: nil) #expect(session.snapshotMakeCount() == 1) @@ -79,11 +83,7 @@ import Testing @Test func concurrentRequestsStillUseSingleWebSocket() async throws { let session = self.makeSession(helloDelayMs: 150) - let url = try #require(URL(string: "ws://example.invalid")) - let cfg = ConfigSource(token: nil) - let conn = GatewayConnection( - configProvider: { (url: url, token: cfg.snapshotToken(), password: nil) }, - sessionBox: WebSocketSessionBox(session: session)) + let (conn, _) = try self.makeConnection(session: session) async let r1: Data = conn.request(method: "status", params: nil) async let r2: Data = conn.request(method: "status", params: nil) @@ -94,11 +94,7 @@ import Testing @Test func subscribeReplaysLatestSnapshot() async throws { let session = self.makeSession() - let url = try #require(URL(string: "ws://example.invalid")) - let cfg = ConfigSource(token: nil) - let conn = GatewayConnection( - configProvider: { (url: url, token: cfg.snapshotToken(), password: nil) }, - sessionBox: WebSocketSessionBox(session: session)) + let (conn, _) = try self.makeConnection(session: session) _ = try await conn.request(method: "status", params: nil) @@ -115,11 +111,7 @@ import Testing @Test func subscribeEmitsSeqGapBeforeEvent() async throws { let session = self.makeSession() - let url = try #require(URL(string: "ws://example.invalid")) - let cfg = ConfigSource(token: nil) - let conn = GatewayConnection( - configProvider: { (url: url, token: cfg.snapshotToken(), password: nil) }, - sessionBox: WebSocketSessionBox(session: session)) + let (conn, _) = try self.makeConnection(session: session) let stream = await conn.subscribe(bufferingNewest: 10) var iterator = stream.makeAsyncIterator() diff --git a/apps/macos/Tests/OpenClawIPCTests/GatewayDiscoveryHelpersTests.swift b/apps/macos/Tests/OpenClawIPCTests/GatewayDiscoveryHelpersTests.swift index 17ffec07d46..de62fa69787 100644 --- a/apps/macos/Tests/OpenClawIPCTests/GatewayDiscoveryHelpersTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/GatewayDiscoveryHelpersTests.swift @@ -27,19 +27,26 @@ struct GatewayDiscoveryHelpersTests { isLocal: false) } - @Test func sshTargetUsesResolvedServiceHostOnly() { - let gateway = self.makeGateway( - serviceHost: "resolved.example.ts.net", - servicePort: 18789, - sshPort: 2201) - + private func assertSSHTarget( + for gateway: GatewayDiscoveryModel.DiscoveredGateway, + host: String, + port: Int) + { guard let target = GatewayDiscoveryHelpers.sshTarget(for: gateway) else { Issue.record("expected ssh target") return } let parsed = CommandResolver.parseSSHTarget(target) - #expect(parsed?.host == "resolved.example.ts.net") - #expect(parsed?.port == 2201) + #expect(parsed?.host == host) + #expect(parsed?.port == port) + } + + @Test func sshTargetUsesResolvedServiceHostOnly() { + let gateway = self.makeGateway( + serviceHost: "resolved.example.ts.net", + servicePort: 18789, + sshPort: 2201) + assertSSHTarget(for: gateway, host: "resolved.example.ts.net", port: 2201) } @Test func sshTargetAllowsMissingResolvedServicePort() { @@ -47,14 +54,7 @@ struct GatewayDiscoveryHelpersTests { serviceHost: "resolved.example.ts.net", servicePort: nil, sshPort: 2201) - - guard let target = GatewayDiscoveryHelpers.sshTarget(for: gateway) else { - Issue.record("expected ssh target") - return - } - let parsed = CommandResolver.parseSSHTarget(target) - #expect(parsed?.host == "resolved.example.ts.net") - #expect(parsed?.port == 2201) + assertSSHTarget(for: gateway, host: "resolved.example.ts.net", port: 2201) } @Test func sshTargetRejectsTxtOnlyGateways() { diff --git a/apps/macos/Tests/OpenClawIPCTests/GatewayEndpointStoreTests.swift b/apps/macos/Tests/OpenClawIPCTests/GatewayEndpointStoreTests.swift index 4bfd203691a..3d7796879f6 100644 --- a/apps/macos/Tests/OpenClawIPCTests/GatewayEndpointStoreTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/GatewayEndpointStoreTests.swift @@ -3,6 +3,22 @@ import Testing @testable import OpenClaw @Suite struct GatewayEndpointStoreTests { + private func makeLaunchAgentSnapshot( + env: [String: String], + token: String?, + password: String?) -> LaunchAgentPlistSnapshot + { + LaunchAgentPlistSnapshot( + programArguments: [], + environment: env, + stdoutPath: nil, + stderrPath: nil, + port: nil, + bind: nil, + token: token, + password: password) + } + private func makeDefaults() -> UserDefaults { let suiteName = "GatewayEndpointStoreTests.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suiteName)! @@ -11,13 +27,8 @@ import Testing } @Test func resolveGatewayTokenPrefersEnvAndFallsBackToLaunchd() { - let snapshot = LaunchAgentPlistSnapshot( - programArguments: [], - environment: ["OPENCLAW_GATEWAY_TOKEN": "launchd-token"], - stdoutPath: nil, - stderrPath: nil, - port: nil, - bind: nil, + let snapshot = self.makeLaunchAgentSnapshot( + env: ["OPENCLAW_GATEWAY_TOKEN": "launchd-token"], token: "launchd-token", password: nil) @@ -37,13 +48,8 @@ import Testing } @Test func resolveGatewayTokenIgnoresLaunchdInRemoteMode() { - let snapshot = LaunchAgentPlistSnapshot( - programArguments: [], - environment: ["OPENCLAW_GATEWAY_TOKEN": "launchd-token"], - stdoutPath: nil, - stderrPath: nil, - port: nil, - bind: nil, + let snapshot = self.makeLaunchAgentSnapshot( + env: ["OPENCLAW_GATEWAY_TOKEN": "launchd-token"], token: "launchd-token", password: nil) @@ -56,13 +62,8 @@ import Testing } @Test func resolveGatewayPasswordFallsBackToLaunchd() { - let snapshot = LaunchAgentPlistSnapshot( - programArguments: [], - environment: ["OPENCLAW_GATEWAY_PASSWORD": "launchd-pass"], - stdoutPath: nil, - stderrPath: nil, - port: nil, - bind: nil, + let snapshot = self.makeLaunchAgentSnapshot( + env: ["OPENCLAW_GATEWAY_PASSWORD": "launchd-pass"], token: nil, password: "launchd-pass") diff --git a/apps/macos/Tests/OpenClawIPCTests/GatewayWebSocketTestSupport.swift b/apps/macos/Tests/OpenClawIPCTests/GatewayWebSocketTestSupport.swift index 2de054da824..bb5d7c12d7a 100644 --- a/apps/macos/Tests/OpenClawIPCTests/GatewayWebSocketTestSupport.swift +++ b/apps/macos/Tests/OpenClawIPCTests/GatewayWebSocketTestSupport.swift @@ -21,15 +21,7 @@ enum GatewayWebSocketTestSupport { } static func connectRequestID(from message: URLSessionWebSocketTask.Message) -> String? { - let data: Data? = switch message { - case let .data(d): d - case let .string(s): s.data(using: .utf8) - @unknown default: nil - } - guard let data else { return nil } - guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { - return nil - } + guard let obj = self.requestFrameObject(from: message) else { return nil } guard (obj["type"] as? String) == "req", (obj["method"] as? String) == "connect" else { return nil } @@ -61,19 +53,21 @@ enum GatewayWebSocketTestSupport { } static func requestID(from message: URLSessionWebSocketTask.Message) -> String? { + guard let obj = self.requestFrameObject(from: message) else { return nil } + guard (obj["type"] as? String) == "req" else { + return nil + } + return obj["id"] as? String + } + + private static func requestFrameObject(from message: URLSessionWebSocketTask.Message) -> [String: Any]? { let data: Data? = switch message { case let .data(d): d case let .string(s): s.data(using: .utf8) @unknown default: nil } guard let data else { return nil } - guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { - return nil - } - guard (obj["type"] as? String) == "req" else { - return nil - } - return obj["id"] as? String + return try? JSONSerialization.jsonObject(with: data) as? [String: Any] } static func okResponseData(id: String) -> Data { diff --git a/apps/macos/Tests/OpenClawIPCTests/OpenClawConfigFileTests.swift b/apps/macos/Tests/OpenClawIPCTests/OpenClawConfigFileTests.swift index 2cd9d6432e2..7c3804eb494 100644 --- a/apps/macos/Tests/OpenClawIPCTests/OpenClawConfigFileTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/OpenClawConfigFileTests.swift @@ -4,12 +4,16 @@ import Testing @Suite(.serialized) struct OpenClawConfigFileTests { - @Test - func configPathRespectsEnvOverride() async { - let override = FileManager().temporaryDirectory + private func makeConfigOverridePath() -> String { + FileManager().temporaryDirectory .appendingPathComponent("openclaw-config-\(UUID().uuidString)") .appendingPathComponent("openclaw.json") .path + } + + @Test + func configPathRespectsEnvOverride() async { + let override = makeConfigOverridePath() await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": override]) { #expect(OpenClawConfigFile.url().path == override) @@ -19,10 +23,7 @@ struct OpenClawConfigFileTests { @MainActor @Test func remoteGatewayPortParsesAndMatchesHost() async { - let override = FileManager().temporaryDirectory - .appendingPathComponent("openclaw-config-\(UUID().uuidString)") - .appendingPathComponent("openclaw.json") - .path + let override = makeConfigOverridePath() await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": override]) { OpenClawConfigFile.saveDict([ @@ -42,10 +43,7 @@ struct OpenClawConfigFileTests { @MainActor @Test func setRemoteGatewayUrlPreservesScheme() async { - let override = FileManager().temporaryDirectory - .appendingPathComponent("openclaw-config-\(UUID().uuidString)") - .appendingPathComponent("openclaw.json") - .path + let override = makeConfigOverridePath() await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": override]) { OpenClawConfigFile.saveDict([ @@ -65,10 +63,7 @@ struct OpenClawConfigFileTests { @MainActor @Test func clearRemoteGatewayUrlRemovesOnlyUrlField() async { - let override = FileManager().temporaryDirectory - .appendingPathComponent("openclaw-config-\(UUID().uuidString)") - .appendingPathComponent("openclaw.json") - .path + let override = makeConfigOverridePath() await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": override]) { OpenClawConfigFile.saveDict([ diff --git a/apps/macos/Tests/OpenClawIPCTests/SkillsSettingsSmokeTests.swift b/apps/macos/Tests/OpenClawIPCTests/SkillsSettingsSmokeTests.swift index 560f3d2f50b..ad2ae573ca2 100644 --- a/apps/macos/Tests/OpenClawIPCTests/SkillsSettingsSmokeTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/SkillsSettingsSmokeTests.swift @@ -2,6 +2,42 @@ import OpenClawProtocol import Testing @testable import OpenClaw +private func makeSkillStatus( + name: String, + description: String, + source: String, + filePath: String, + skillKey: String, + primaryEnv: String? = nil, + emoji: String, + homepage: String? = nil, + disabled: Bool = false, + eligible: Bool, + requirements: SkillRequirements = SkillRequirements(bins: [], env: [], config: []), + missing: SkillMissing = SkillMissing(bins: [], env: [], config: []), + configChecks: [SkillStatusConfigCheck] = [], + install: [SkillInstallOption] = []) + -> SkillStatus +{ + SkillStatus( + name: name, + description: description, + source: source, + filePath: filePath, + baseDir: "/tmp/skills", + skillKey: skillKey, + primaryEnv: primaryEnv, + emoji: emoji, + homepage: homepage, + always: false, + disabled: disabled, + eligible: eligible, + requirements: requirements, + missing: missing, + configChecks: configChecks, + install: install) +} + @Suite(.serialized) @MainActor struct SkillsSettingsSmokeTests { @@ -9,18 +45,15 @@ struct SkillsSettingsSmokeTests { let model = SkillsSettingsModel() model.statusMessage = "Loaded" model.skills = [ - SkillStatus( + makeSkillStatus( name: "Needs Setup", description: "Missing bins and env", source: "openclaw-managed", filePath: "/tmp/skills/needs-setup", - baseDir: "/tmp/skills", skillKey: "needs-setup", primaryEnv: "API_KEY", emoji: "🧰", homepage: "https://example.com/needs-setup", - always: false, - disabled: false, eligible: false, requirements: SkillRequirements( bins: ["python3"], @@ -36,43 +69,29 @@ struct SkillsSettingsSmokeTests { install: [ SkillInstallOption(id: "brew", kind: "brew", label: "brew install python", bins: ["python3"]), ]), - SkillStatus( + makeSkillStatus( name: "Ready Skill", description: "All set", source: "openclaw-bundled", filePath: "/tmp/skills/ready", - baseDir: "/tmp/skills", skillKey: "ready", - primaryEnv: nil, emoji: "✅", homepage: "https://example.com/ready", - always: false, - disabled: false, eligible: true, - requirements: SkillRequirements(bins: [], env: [], config: []), - missing: SkillMissing(bins: [], env: [], config: []), configChecks: [ SkillStatusConfigCheck(path: "skills.ready", value: AnyCodable(true), satisfied: true), SkillStatusConfigCheck(path: "skills.limit", value: AnyCodable(5), satisfied: true), ], install: []), - SkillStatus( + makeSkillStatus( name: "Disabled Skill", description: "Disabled in config", source: "openclaw-extra", filePath: "/tmp/skills/disabled", - baseDir: "/tmp/skills", skillKey: "disabled", - primaryEnv: nil, emoji: "🚫", - homepage: nil, - always: false, disabled: true, - eligible: false, - requirements: SkillRequirements(bins: [], env: [], config: []), - missing: SkillMissing(bins: [], env: [], config: []), - configChecks: [], - install: []), + eligible: false), ] let state = AppState(preview: true) @@ -87,23 +106,14 @@ struct SkillsSettingsSmokeTests { @Test func skillsSettingsBuildsBodyWithLocalMode() { let model = SkillsSettingsModel() model.skills = [ - SkillStatus( + makeSkillStatus( name: "Local Skill", description: "Local ready", source: "openclaw-workspace", filePath: "/tmp/skills/local", - baseDir: "/tmp/skills", skillKey: "local", - primaryEnv: nil, emoji: "🏠", - homepage: nil, - always: false, - disabled: false, - eligible: true, - requirements: SkillRequirements(bins: [], env: [], config: []), - missing: SkillMissing(bins: [], env: [], config: []), - configChecks: [], - install: []), + eligible: true), ] let state = AppState(preview: true) diff --git a/apps/macos/Tests/OpenClawIPCTests/TestIsolation.swift b/apps/macos/Tests/OpenClawIPCTests/TestIsolation.swift index 1002b7ed307..8be68afed24 100644 --- a/apps/macos/Tests/OpenClawIPCTests/TestIsolation.swift +++ b/apps/macos/Tests/OpenClawIPCTests/TestIsolation.swift @@ -34,6 +34,26 @@ enum TestIsolation { 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 { @@ -58,37 +78,13 @@ enum TestIsolation { do { let result = try await body() - 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) - } - } + restoreUserDefaults(previousDefaults, userDefaults: userDefaults) + restoreEnv(previousEnv) 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) - } - } + restoreUserDefaults(previousDefaults, userDefaults: userDefaults) + restoreEnv(previousEnv) await TestIsolationLock.shared.release() throw error } diff --git a/apps/macos/Tests/OpenClawIPCTests/VoiceWakeGlobalSettingsSyncTests.swift b/apps/macos/Tests/OpenClawIPCTests/VoiceWakeGlobalSettingsSyncTests.swift index 1d95bb47050..d19a9ccc25f 100644 --- a/apps/macos/Tests/OpenClawIPCTests/VoiceWakeGlobalSettingsSyncTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/VoiceWakeGlobalSettingsSyncTests.swift @@ -4,20 +4,26 @@ import Testing @testable import OpenClaw @Suite(.serialized) struct VoiceWakeGlobalSettingsSyncTests { - @Test func appliesVoiceWakeChangedEventToAppState() async { - let previous = await MainActor.run { AppStateStore.shared.swabbleTriggerWords } - - await MainActor.run { - AppStateStore.shared.applyGlobalVoiceWakeTriggers(["before"]) - } - - let payload = OpenClawProtocol.AnyCodable(["triggers": ["openclaw", "computer"]]) - let evt = EventFrame( + private func voiceWakeChangedEvent(payload: OpenClawProtocol.AnyCodable) -> EventFrame { + EventFrame( type: "event", event: "voicewake.changed", payload: payload, seq: nil, stateversion: nil) + } + + private func applyTriggersAndCapturePrevious(_ triggers: [String]) async -> [String] { + let previous = await MainActor.run { AppStateStore.shared.swabbleTriggerWords } + await MainActor.run { + AppStateStore.shared.applyGlobalVoiceWakeTriggers(triggers) + } + return previous + } + + @Test func appliesVoiceWakeChangedEventToAppState() async { + let previous = await applyTriggersAndCapturePrevious(["before"]) + let evt = voiceWakeChangedEvent(payload: OpenClawProtocol.AnyCodable(["triggers": ["openclaw", "computer"]])) await VoiceWakeGlobalSettingsSync.shared.handle(push: .event(evt)) @@ -30,19 +36,8 @@ import Testing } @Test func ignoresVoiceWakeChangedEventWithInvalidPayload() async { - let previous = await MainActor.run { AppStateStore.shared.swabbleTriggerWords } - - await MainActor.run { - AppStateStore.shared.applyGlobalVoiceWakeTriggers(["before"]) - } - - let payload = OpenClawProtocol.AnyCodable(["unexpected": 123]) - let evt = EventFrame( - type: "event", - event: "voicewake.changed", - payload: payload, - seq: nil, - stateversion: nil) + let previous = await applyTriggersAndCapturePrevious(["before"]) + let evt = voiceWakeChangedEvent(payload: OpenClawProtocol.AnyCodable(["unexpected": 123])) await VoiceWakeGlobalSettingsSync.shared.handle(push: .event(evt))