refactor(tests): dedupe macos ipc smoke setup blocks

This commit is contained in:
Peter Steinberger
2026-03-02 09:55:34 +00:00
parent 8553d22428
commit fc692d82fd
14 changed files with 280 additions and 335 deletions

View File

@@ -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)

View File

@@ -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",

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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() {

View File

@@ -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")

View File

@@ -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 {

View File

@@ -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([

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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))