mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor(tests): dedupe macos ipc smoke setup blocks
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user