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