Files
openclaw/apps/macos/Tests/OpenClawIPCTests/MacNodeRuntimeTests.swift
2026-08-11 22:12:56 -07:00

907 lines
37 KiB
Swift

import CoreLocation
import Dispatch
import Foundation
import OpenClawKit
import Testing
@testable import OpenClaw
struct MacNodeRuntimeTests {
private final class LockedCounter: @unchecked Sendable {
private let lock = NSLock()
private var count = 0
func increment() {
self.lock.lock()
self.count += 1
self.lock.unlock()
}
func value() -> Int {
self.lock.lock()
defer { self.lock.unlock() }
return self.count
}
}
private final class CatalogWorkerProbe: @unchecked Sendable {
private let lock = NSLock()
private let releaseFirst = DispatchSemaphore(value: 0)
private var calls = 0
private var active = 0
private var peakActive = 0
func run() -> String {
self.lock.lock()
self.calls += 1
let call = self.calls
self.active += 1
self.peakActive = max(self.peakActive, self.active)
self.lock.unlock()
if call == 1 {
self.releaseFirst.wait()
}
self.lock.lock()
self.active -= 1
self.lock.unlock()
return "call-\(call)"
}
func release() {
self.releaseFirst.signal()
}
func snapshot() -> (calls: Int, peakActive: Int) {
self.lock.lock()
defer { self.lock.unlock() }
return (self.calls, self.peakActive)
}
}
private func waitForCount(_ expected: Int, counter: LockedCounter) async -> Bool {
let clock = ContinuousClock()
let deadline = clock.now.advanced(by: .seconds(10))
while counter.value() < expected, clock.now < deadline {
await Task.yield()
}
return counter.value() >= expected
}
private func invoke(
_ runtime: MacNodeRuntime,
_ id: String,
_ command: String,
_ paramsJSON: String? = nil,
nodeId: String? = nil) async -> BridgeInvokeResponse
{
await runtime.handleInvoke(BridgeInvokeRequest(
id: id,
command: command,
paramsJSON: paramsJSON,
nodeId: nodeId))
}
private func invoke(
_ runtime: MacNodeRuntime,
_ id: String,
_ command: String,
params: some Encodable,
nodeId: String? = nil) async throws -> BridgeInvokeResponse
{
return await self.invoke(
runtime,
id,
command,
String(decoding: try JSONEncoder().encode(params), as: UTF8.self),
nodeId: nodeId)
}
actor CanvasRefreshProbe {
private(set) var calls = 0
func refresh() -> String? {
self.calls += 1
return "http://127.0.0.1:18789/__openclaw__/cap/refreshed-token"
}
}
actor CanvasReconnectProbe {
private var surfaceURL = "http://127.0.0.1:18789/__openclaw__/cap/old-token"
func current() -> String? {
self.surfaceURL
}
func reconnectDuringRefresh() -> String? {
self.surfaceURL = "http://127.0.0.1:18789/__openclaw__/cap/new-token"
return nil
}
}
@MainActor
final class MainActorServicesProbe: MacNodeRuntimeMainActorServices, @unchecked Sendable {
typealias SnapshotInspection = (Int?, Int?, Double?, OpenClawScreenSnapshotFormat?) -> Void
var snapshotCallCount = 0
var receivedSnapshotParams: MacNodeScreenSnapshotParams?
var snapshotResult: ScreenSnapshotResult
var snapshotError: Error?
var snapshotCalledAtMs: Int64?
var receivedParams: OpenClawComputerActParams?
var actError: Error?
var performCallCount = 0
var releaseCallCount = 0
var locationStatus: CLAuthorizationStatus
var receivedLifecycleGenerations: [UInt64] = []
var receivedReleaseGenerations: [UInt64] = []
private let snapshotInspection: SnapshotInspection?
private let performEnteredGate: AsyncTestGate?
private let allowPerformGate: AsyncTestGate?
private var latestLifecycleGeneration: UInt64 = 0
init(
snapshotResult: ScreenSnapshotResult = ScreenSnapshotResult(
data: Data("ok".utf8),
format: .jpeg,
width: 10,
height: 10,
displayFrameId: "display-frame-test"),
snapshotError: Error? = nil,
snapshotInspection: SnapshotInspection? = nil,
actError: Error? = nil,
locationAuthorizationStatus: CLAuthorizationStatus = .authorizedAlways,
performEnteredGate: AsyncTestGate? = nil,
allowPerformGate: AsyncTestGate? = nil)
{
self.snapshotResult = snapshotResult
self.snapshotError = snapshotError
self.snapshotInspection = snapshotInspection
self.actError = actError
self.locationStatus = locationAuthorizationStatus
self.performEnteredGate = performEnteredGate
self.allowPerformGate = allowPerformGate
}
func snapshotScreen(
screenIndex: Int?,
maxWidth: Int?,
quality: Double?,
format: OpenClawScreenSnapshotFormat?) async throws -> ScreenSnapshotResult
{
self.snapshotCallCount += 1
self.snapshotCalledAtMs = Int64(Date().timeIntervalSince1970 * 1000)
self.receivedSnapshotParams = MacNodeScreenSnapshotParams(
screenIndex: screenIndex,
maxWidth: maxWidth,
quality: quality,
format: format)
self.snapshotInspection?(screenIndex, maxWidth, quality, format)
if let snapshotError {
throw snapshotError
}
return self.snapshotResult
}
func recordScreen(
screenIndex _: Int?,
durationMs _: Int?,
fps _: Double?,
includeAudio _: Bool?,
outPath _: String?) async throws -> (path: String, hasAudio: Bool)
{
let url = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-test-screen-record-\(UUID().uuidString).mp4")
try Data("ok".utf8).write(to: url)
return (path: url.path, hasAudio: false)
}
func locationAuthorizationStatus() -> CLAuthorizationStatus {
self.locationStatus
}
func locationAccuracyAuthorization() -> CLAccuracyAuthorization {
.fullAccuracy
}
func currentLocation(
desiredAccuracy: OpenClawLocationAccuracy,
maxAgeMs: Int?,
timeoutMs: Int?) async throws -> CLLocation
{
_ = desiredAccuracy
_ = maxAgeMs
_ = timeoutMs
return CLLocation(latitude: 0, longitude: 0)
}
func performComputerAct(
_ params: OpenClawComputerActParams,
lifecycleGeneration: UInt64) async throws -> OpenClawComputerActResult
{
self.performCallCount += 1
self.receivedParams = params
self.receivedLifecycleGenerations.append(lifecycleGeneration)
self.performEnteredGate?.open()
await self.allowPerformGate?.wait()
guard lifecycleGeneration >= self.latestLifecycleGeneration else {
throw ComputerActionService.ComputerActionError.lifecycleChanged
}
if lifecycleGeneration > self.latestLifecycleGeneration {
self.latestLifecycleGeneration = lifecycleGeneration
}
if let actError {
throw actError
}
return OpenClawComputerActResult(ok: true, cursorX: params.x ?? 0, cursorY: params.y ?? 0)
}
func releaseHeldInput(lifecycleGeneration: UInt64) async {
guard lifecycleGeneration > self.latestLifecycleGeneration else { return }
self.latestLifecycleGeneration = lifecycleGeneration
self.receivedReleaseGenerations.append(lifecycleGeneration)
self.releaseCallCount += 1
}
}
@Test func `handle invoke rejects unknown command`() async {
let runtime = MacNodeRuntime()
let response = await self.invoke(runtime, "req-1", "unknown.command")
#expect(response.ok == false)
}
@Test func `handle invoke returns injected Codex thread catalog`() async {
let payload = #"{"sessions":[]}"#
let runtime = MacNodeRuntime(
codexThreadCatalogEnabled: { true },
codexThreadListRequest: { paramsJSON in
#expect(paramsJSON == #"{"limit":7}"#)
return payload
})
let response = await self.invoke(
runtime, "req-codex-threads", MacNodeCodexThreadCatalogContract.listCommand, #"{"limit":7}"#)
#expect(response.ok)
#expect(response.payloadJSON == payload)
}
@Test func `handle invoke enforces local Codex catalog consent`() async {
let runtime = MacNodeRuntime(
codexThreadCatalogEnabled: { false },
codexThreadListRequest: { _ in
Issue.record("disabled Codex catalog request must not execute")
return #"{"sessions":[]}"#
})
let response = await self.invoke(
runtime, "req-codex-disabled", MacNodeCodexThreadCatalogContract.listCommand)
#expect(!response.ok)
#expect(response.error?.code == .unavailable)
#expect(response.error?.message == "UNAVAILABLE: Codex session catalog is disabled")
}
@Test func `handle invoke returns an injected Codex transcript turn page`() async {
let payload = #"{"data":[{"id":"item-1","type":"agentMessage","text":"answer"}],"nextCursor":"page-2"}"#
let runtime = MacNodeRuntime(
codexThreadCatalogEnabled: { true },
codexThreadTurnsRequest: { paramsJSON in
#expect(paramsJSON == #"{"threadId":"thread-1","limit":50}"#)
return payload
})
let response = await self.invoke(
runtime, "req-codex-items", MacNodeCodexThreadCatalogContract.turnsCommand,
#"{"threadId":"thread-1","limit":50}"#)
#expect(response.ok)
#expect(response.payloadJSON == payload)
}
@Test func `handle invoke returns injected Claude session pages`() async {
let listPayload = #"{"sessions":[]}"#
let readPayload = #"{"threadId":"thread-1","items":[]}"#
let runtime = MacNodeRuntime(
claudeSessionCatalogEnabled: { true },
claudeSessionListRequest: { paramsJSON in
#expect(paramsJSON == #"{"limit":7}"#)
return listPayload
},
claudeSessionReadRequest: { paramsJSON in
#expect(paramsJSON == #"{"threadId":"thread-1","limit":20}"#)
return readPayload
})
let list = await self.invoke(
runtime, "req-claude-list", MacNodeClaudeSessionCatalogContract.listCommand, #"{"limit":7}"#)
let read = await self.invoke(
runtime, "req-claude-read", MacNodeClaudeSessionCatalogContract.readCommand,
#"{"threadId":"thread-1","limit":20}"#)
#expect(list.ok)
#expect(list.payloadJSON == listPayload)
#expect(read.ok)
#expect(read.payloadJSON == readPayload)
}
@Test func `Claude catalog worker serializes filesystem operations`() async throws {
let probe = CatalogWorkerProbe()
let worker = MacNodeClaudeSessionCatalogWorker(
listOperation: { _ in probe.run() },
readOperation: { _ in probe.run() })
let first = Task { try await worker.list(paramsJSON: nil) }
while probe.snapshot().calls == 0 {
await Task.yield()
}
let second = Task { try await worker.read(paramsJSON: nil) }
try await Task.sleep(for: .milliseconds(50))
#expect(probe.snapshot().calls == 1)
let cancelStarted = ContinuousClock.now
let watchdog = Task {
try await Task.sleep(for: .seconds(1))
probe.release()
}
second.cancel()
await #expect(throws: CancellationError.self) {
try await second.value
}
#expect(ContinuousClock.now - cancelStarted < .milliseconds(500))
watchdog.cancel()
probe.release()
#expect(try await first.value == "call-1")
#expect(try await worker.read(paramsJSON: nil) == "call-2")
#expect(probe.snapshot().peakActive == 1)
}
@Test func `Claude catalog worker propagates caller cancellation`() async {
let started = AsyncStream<Void>.makeStream()
let worker = MacNodeClaudeSessionCatalogWorker(
listOperation: { _ in
started.continuation.yield()
while !Task.isCancelled {
Thread.sleep(forTimeInterval: 0.001)
}
throw CancellationError()
},
readOperation: { _ in "unused" })
let task = Task { try await worker.list(paramsJSON: nil) }
let watchdog = Task {
try? await Task.sleep(for: .seconds(5))
started.continuation.finish()
}
var iterator = started.stream.makeAsyncIterator()
#expect(await iterator.next() != nil)
watchdog.cancel()
started.continuation.finish()
task.cancel()
await #expect(throws: CancellationError.self) {
try await task.value
}
}
@Test func `handle invoke enforces local Claude catalog policy`() async {
let runtime = MacNodeRuntime(
claudeSessionCatalogEnabled: { false },
claudeSessionListRequest: { _ in
Issue.record("disabled Claude catalog request must not execute")
return #"{"sessions":[]}"#
})
let response = await self.invoke(
runtime, "req-claude-disabled", MacNodeClaudeSessionCatalogContract.listCommand)
#expect(!response.ok)
#expect(response.error?.code == .unavailable)
#expect(response.error?.message == "UNAVAILABLE: Claude session catalog is disabled")
}
@Test func `A2UI host capability refresh uses injected node session refresher`() async {
let probe = CanvasRefreshProbe()
let resolver = MacNodeCanvasHostedSurfaceResolver(
currentSurfaceURL: { "http://127.0.0.1:18789/__openclaw__/cap/current-token" },
refreshSurfaceURL: { _ in await probe.refresh() })
let current = await resolver.resolveA2UIURL()
#expect(current ==
"http://127.0.0.1:18789/__openclaw__/cap/current-token/__openclaw__/a2ui/?platform=macos")
#expect(await probe.calls == 0)
let refreshed = await resolver.resolveA2UIURL(forceRefresh: true)
#expect(refreshed ==
"http://127.0.0.1:18789/__openclaw__/cap/refreshed-token/__openclaw__/a2ui/?platform=macos")
#expect(await probe.calls == 1)
}
@Test func `hosted Canvas commands refresh capability and preserve target components`() async throws {
let probe = CanvasRefreshProbe()
let resolver = MacNodeCanvasHostedSurfaceResolver(
currentSurfaceURL: { "http://127.0.0.1:18789/__openclaw__/cap/current-token" },
refreshSurfaceURL: { _ in await probe.refresh() })
let resolved = try await resolver.resolveTarget(
"/__openclaw__/canvas/demo%20page.html?mode=proof#result")
#expect(resolved?.url.absoluteString ==
"http://127.0.0.1:18789/__openclaw__/cap/refreshed-token/__openclaw__/canvas/demo%20page.html?mode=proof#result")
#expect(resolved?.allowsA2UIActions == false)
#expect(await probe.calls == 1)
let external = try await resolver.resolveTarget("https://example.com/")
#expect(external == nil)
#expect(await probe.calls == 1)
}
@Test func `hosted Canvas commands use replacement route after refresh fails`() async throws {
let probe = CanvasReconnectProbe()
let resolver = MacNodeCanvasHostedSurfaceResolver(
currentSurfaceURL: { await probe.current() },
refreshSurfaceURL: { _ in await probe.reconnectDuringRefresh() })
let resolved = try await resolver.resolveTarget("/__openclaw__/canvas/demo.html")
#expect(resolved?.url.absoluteString ==
"http://127.0.0.1:18789/__openclaw__/cap/new-token/__openclaw__/canvas/demo.html")
}
@Test func `handle invoke rejects empty notification`() async throws {
let runtime = MacNodeRuntime()
let params = OpenClawSystemNotifyParams(title: "", body: "")
let response = try await self.invoke(
runtime, "req-3", OpenClawSystemCommand.notify.rawValue, params: params)
#expect(response.ok == false)
}
@Test func `handle invoke camera list requires enabled camera`() async {
await TestIsolation.withUserDefaultsValues([cameraEnabledKey: false]) {
let runtime = MacNodeRuntime()
let response = await self.invoke(runtime, "req-4", OpenClawCameraCommand.list.rawValue)
#expect(response.ok == false)
#expect(response.error?.message.contains("CAMERA_DISABLED") == true)
}
}
@Test func `handle location invoke applies authorization required by mode`() async throws {
let authorizedWhenInUse = try #require(CLAuthorizationStatus(rawValue: 4))
let cases: [(mode: OpenClawLocationMode, status: CLAuthorizationStatus, accepted: Bool)] = [
(.whileUsing, authorizedWhenInUse, true),
(.always, authorizedWhenInUse, false),
(.whileUsing, .authorizedAlways, true),
(.always, .authorizedAlways, true),
]
for testCase in cases {
await TestIsolation.withUserDefaultsValues([locationModeKey: testCase.mode.rawValue]) {
let services = await MainActor.run {
MainActorServicesProbe(locationAuthorizationStatus: testCase.status)
}
let runtime = MacNodeRuntime(makeMainActorServices: { services })
let response = await self.invoke(
runtime, "req-location", OpenClawLocationCommand.get.rawValue)
#expect(response.ok == testCase.accepted)
}
}
}
@Test func `handle invoke screen record uses injected services`() async throws {
let services = await MainActor.run { MainActorServicesProbe() }
let runtime = MacNodeRuntime(makeMainActorServices: { services })
let params = MacNodeScreenRecordParams(durationMs: 250)
let response = try await self.invoke(
runtime, "req-5", MacNodeScreenCommand.record.rawValue, params: params)
#expect(response.ok == true)
let payloadJSON = try #require(response.payloadJSON)
struct Payload: Decodable {
var format: String
var base64: String
}
let payload = try JSONDecoder().decode(Payload.self, from: Data(payloadJSON.utf8))
#expect(payload.format == "mp4")
#expect(!payload.base64.isEmpty)
}
@Test func `handle invoke screen snapshot uses injected services`() async throws {
let services = await MainActor.run {
MainActorServicesProbe(
snapshotResult: ScreenSnapshotResult(
data: Data("ok".utf8),
format: .jpeg,
width: 800,
height: 450,
displayFrameId: "display-frame-test"),
snapshotInspection: { screenIndex, maxWidth, quality, _ in
#expect(screenIndex == 0)
#expect(maxWidth == 800)
#expect(quality == 0.5)
})
}
let runtime = MacNodeRuntime(makeMainActorServices: { services })
let params = MacNodeScreenSnapshotParams(
screenIndex: 0,
maxWidth: 800,
quality: 0.5,
format: .jpeg)
let response = try await self.invoke(
runtime, "req-screen-snapshot", MacNodeScreenCommand.snapshot.rawValue, params: params)
#expect(response.ok == true)
let payloadJSON = try #require(response.payloadJSON)
struct Payload: Decodable {
var format: String
var base64: String
var displayFrameId: String
var width: Int
var height: Int
var capturedAtMs: Int64
}
let payload = try JSONDecoder().decode(Payload.self, from: Data(payloadJSON.utf8))
#expect(payload.format == "jpeg")
#expect(payload.base64 == Data("ok".utf8).base64EncodedString())
#expect(payload.displayFrameId == "display-frame-test")
#expect(payload.width == 800)
#expect(payload.height == 450)
#expect(payload.capturedAtMs > 0)
let snapshotCalledAtMs = await MainActor.run { services.snapshotCalledAtMs }
#expect(snapshotCalledAtMs != nil)
#expect(payload.capturedAtMs <= snapshotCalledAtMs!)
}
@Test func `handle invoke screen snapshot rejects malformed params before capture`() async {
let services = await MainActor.run { MainActorServicesProbe() }
let runtime = MacNodeRuntime(makeMainActorServices: { services })
let response = await self.invoke(
runtime, "req-screen-snapshot-invalid", MacNodeScreenCommand.snapshot.rawValue, #"{"screenIndex":"#)
#expect(response.ok == false)
#expect(response.error?.code == .invalidRequest)
#expect(response.error?.message == "INVALID_REQUEST: invalid screen snapshot params")
let snapshotCallCount = await MainActor.run { services.snapshotCallCount }
#expect(snapshotCallCount == 0)
}
@Test func `handle invoke screen snapshot keeps nil params as defaults`() async {
let services = await MainActor.run { MainActorServicesProbe() }
let runtime = MacNodeRuntime(makeMainActorServices: { services })
let response = await self.invoke(
runtime, "req-screen-snapshot-defaults", MacNodeScreenCommand.snapshot.rawValue)
#expect(response.ok == true)
let received = await MainActor.run { services.receivedSnapshotParams }
#expect(received == MacNodeScreenSnapshotParams())
}
@Test func `handle invoke rejects computer act when control disabled`() async throws {
let services = await MainActor.run { MainActorServicesProbe() }
let runtime = MacNodeRuntime(
makeMainActorServices: { services },
computerControlEnabled: { false })
let params = OpenClawComputerActParams(action: .leftClick, x: 5, y: 6, refWidth: 1280)
let response = try await self.invoke(
runtime, "req-computer-disabled", OpenClawComputerCommand.act.rawValue, params: params)
#expect(response.ok == false)
#expect(response.error?.code == .unavailable)
#expect(response.error?.message == "COMPUTER_DISABLED: enable Computer Control in Settings")
let received = await MainActor.run { services.receivedParams }
#expect(received == nil)
}
@Test func `handle invoke routes computer act to the injected services when enabled`() async throws {
let services = await MainActor.run { MainActorServicesProbe() }
let runtime = MacNodeRuntime(
makeMainActorServices: { services },
computerControlEnabled: { true })
let params = OpenClawComputerActParams(action: .leftClick, x: 12, y: 34, refWidth: 1280)
let response = try await self.invoke(
runtime, "req-computer-ok", OpenClawComputerCommand.act.rawValue, params: params)
#expect(response.ok == true)
let received = await MainActor.run { services.receivedParams }
#expect(received?.action == .leftClick)
#expect(received?.x == 12)
let payloadJSON = try #require(response.payloadJSON)
let result = try JSONDecoder().decode(OpenClawComputerActResult.self, from: Data(payloadJSON.utf8))
#expect(result.ok == true)
#expect(result.cursorX == 12)
}
@Test func `concurrent invokes share one main actor services initialization`() async throws {
let services = await MainActor.run { MainActorServicesProbe() }
let factoryGate = AsyncTestGate()
defer { factoryGate.open() }
let factoryCalls = LockedCounter()
let admissionCalls = LockedCounter()
let runtime = MacNodeRuntime(
makeMainActorServices: {
factoryCalls.increment()
await factoryGate.wait()
return services
},
computerControlEnabled: {
admissionCalls.increment()
return true
})
let params = OpenClawComputerActParams(action: .leftClick, x: 12, y: 34, refWidth: 1280)
let json = try String(data: JSONEncoder().encode(params), encoding: .utf8)
let first = Task {
await self.invoke(runtime, "req-computer-single-flight-1", OpenClawComputerCommand.act.rawValue, json)
}
try #require(await self.waitForCount(1, counter: factoryCalls))
let second = Task {
await self.invoke(runtime, "req-computer-single-flight-2", OpenClawComputerCommand.act.rawValue, json)
}
try #require(await self.waitForCount(2, counter: admissionCalls))
// The actor barrier proves the second invoke reached its first suspension.
await runtime.updateMainSessionKey("single-flight-barrier")
#expect(factoryCalls.value() == 1)
factoryGate.open()
let firstResponse = await first.value
let secondResponse = await second.value
#expect(firstResponse.ok)
#expect(secondResponse.ok)
}
@Test func `lifecycle release invalidates first invoke awaiting service initialization`() async throws {
let services = await MainActor.run { MainActorServicesProbe() }
let factoryGate = AsyncTestGate()
defer { factoryGate.open() }
let factoryCalls = LockedCounter()
let runtime = MacNodeRuntime(
makeMainActorServices: {
factoryCalls.increment()
await factoryGate.wait()
return services
},
computerControlEnabled: { true })
let params = OpenClawComputerActParams(action: .leftMouseDown, x: 12, y: 34, refWidth: 1280)
let json = try String(data: JSONEncoder().encode(params), encoding: .utf8)
let invoke = Task {
await self.invoke(runtime, "req-computer-release-during-init", OpenClawComputerCommand.act.rawValue, json)
}
try #require(await self.waitForCount(1, counter: factoryCalls))
await runtime.releaseHeldComputerInput()
factoryGate.open()
let response = await invoke.value
let counts = await MainActor.run {
(perform: services.performCallCount, release: services.releaseCallCount)
}
#expect(!response.ok)
#expect(response.error?.code == .unavailable)
#expect(response.error?.message == "UNAVAILABLE: computer control lifecycle changed")
#expect(counts.perform == 0)
#expect(counts.release == 0)
}
@Test func `lifecycle release after runtime admission invalidates service execution`() async throws {
let performEntered = AsyncTestGate()
let allowPerform = AsyncTestGate()
defer {
performEntered.open()
allowPerform.open()
}
let services = await MainActor.run {
MainActorServicesProbe(
performEnteredGate: performEntered,
allowPerformGate: allowPerform)
}
let runtime = MacNodeRuntime(
makeMainActorServices: { services },
computerControlEnabled: { true })
let params = OpenClawComputerActParams(action: .leftMouseDown, x: 12, y: 34, refWidth: 1280)
let json = try String(data: JSONEncoder().encode(params), encoding: .utf8)
let invoke = Task {
await self.invoke(runtime, "req-computer-release-after-admission", OpenClawComputerCommand.act.rawValue, json)
}
await performEntered.wait()
await runtime.releaseHeldComputerInput()
allowPerform.open()
let response = await invoke.value
let generations = await MainActor.run {
(
perform: services.receivedLifecycleGenerations,
release: services.receivedReleaseGenerations)
}
#expect(!response.ok)
#expect(response.error?.code == .unavailable)
#expect(response.error?.message.contains("lifecycle changed") == true)
#expect(generations.perform == [0])
#expect(generations.release == [1])
}
@Test func `handle invoke maps accessibility denial to unavailable`() async throws {
let services = await MainActor.run {
MainActorServicesProbe(actError: ComputerActionService.ComputerActionError.accessibilityNotTrusted)
}
let runtime = MacNodeRuntime(
makeMainActorServices: { services },
computerControlEnabled: { true })
let params = OpenClawComputerActParams(action: .leftClick, x: 1, y: 1, refWidth: 1280)
let response = try await self.invoke(
runtime, "req-computer-ax", OpenClawComputerCommand.act.rawValue, params: params)
#expect(response.ok == false)
#expect(response.error?.code == .unavailable)
#expect(response.error?.message == "ACCESSIBILITY_REQUIRED: grant Accessibility permission to OpenClaw")
}
@Test func `handle invoke rejects malformed computer act params`() async {
let services = await MainActor.run { MainActorServicesProbe() }
let runtime = MacNodeRuntime(
makeMainActorServices: { services },
computerControlEnabled: { true })
let response = await self.invoke(
runtime, "req-computer-bad", OpenClawComputerCommand.act.rawValue, #"{"action":"#)
#expect(response.ok == false)
#expect(response.error?.code == .invalidRequest)
#expect(response.error?.message == "INVALID_REQUEST: invalid computer.act params")
}
@Test func `handle invoke screen snapshot sanitizes capture failures`() async {
struct SensitiveError: LocalizedError {
let detail: String
var errorDescription: String? {
self.detail
}
}
let services = await MainActor.run {
MainActorServicesProbe(snapshotError: SensitiveError(detail: "TCC_DENIED display-id=ABC123"))
}
let runtime = MacNodeRuntime(makeMainActorServices: { services })
let response = await self.invoke(
runtime, "req-screen-snapshot-error", MacNodeScreenCommand.snapshot.rawValue)
#expect(response.ok == false)
#expect(response.error?.code == .unavailable)
#expect(response.error?.message == "UNAVAILABLE: screen snapshot failed")
}
@Test func `handle invoke screen snapshot reports validation failures as invalid request`() async {
let invalidIndexServices = await MainActor.run {
MainActorServicesProbe(
snapshotError: ScreenSnapshotService.ScreenSnapshotError.invalidScreenIndex(4))
}
let invalidIndexRuntime = MacNodeRuntime(makeMainActorServices: { invalidIndexServices })
let invalidIndexResponse = await self.invoke(
invalidIndexRuntime, "req-screen-snapshot-bad-index", MacNodeScreenCommand.snapshot.rawValue)
#expect(invalidIndexResponse.ok == false)
#expect(invalidIndexResponse.error?.code == .invalidRequest)
#expect(invalidIndexResponse.error?.message == "INVALID_REQUEST: invalid screen index 4")
let noDisplaysServices = await MainActor.run {
MainActorServicesProbe(snapshotError: ScreenSnapshotService.ScreenSnapshotError.noDisplays)
}
let noDisplaysRuntime = MacNodeRuntime(makeMainActorServices: { noDisplaysServices })
let noDisplaysResponse = await self.invoke(
noDisplaysRuntime, "req-screen-snapshot-no-displays", MacNodeScreenCommand.snapshot.rawValue)
#expect(noDisplaysResponse.ok == false)
#expect(noDisplaysResponse.error?.code == .invalidRequest)
#expect(
noDisplaysResponse.error?.message ==
"INVALID_REQUEST: no displays available for screen snapshot")
}
@Test func `handle invoke screen snapshot rejects raw payloads above base64 ceiling`() async {
let payloadSize = 19_660_801
let services = await MainActor.run {
MainActorServicesProbe(snapshotResult: ScreenSnapshotResult(
data: Data(repeating: 0x41, count: payloadSize),
format: .jpeg,
width: 4000,
height: 3000,
displayFrameId: "display-frame-test"))
}
let runtime = MacNodeRuntime(makeMainActorServices: { services })
let response = await self.invoke(
runtime, "req-screen-snapshot-too-large", MacNodeScreenCommand.snapshot.rawValue)
#expect(response.ok == false)
#expect(response.payloadJSON == nil)
#expect(response.error?.code == .unavailable)
#expect(
response.error?.message ==
"UNAVAILABLE: screen snapshot payload too large; reduce maxWidth or use jpeg")
}
@Test func `handle invoke screen snapshot rejects escaped oversized outer frames`() async {
let payloadSize = 12 * 1024 * 1024
let services = await MainActor.run {
MainActorServicesProbe(snapshotResult: ScreenSnapshotResult(
data: Data(repeating: 0xFF, count: payloadSize),
format: .png,
width: 4000,
height: 3000,
displayFrameId: "display-frame-test"))
}
let runtime = MacNodeRuntime(makeMainActorServices: { services })
let response = await self.invoke(
runtime, "req-screen-snapshot-slash-heavy", MacNodeScreenCommand.snapshot.rawValue,
nodeId: "node-slash-heavy")
#expect(response.ok == false)
#expect(response.error?.code == .unavailable)
#expect(
response.error?.message ==
"UNAVAILABLE: screen snapshot payload too large; reduce maxWidth or use jpeg")
}
@Test func `handle invoke screen snapshot accepts near-limit frames that fit`() async throws {
let payloadSize = 19_660_100
let services = await MainActor.run {
MainActorServicesProbe(snapshotResult: ScreenSnapshotResult(
data: Data(repeating: 0x00, count: payloadSize),
format: .jpeg,
width: 4000,
height: 3000,
displayFrameId: "display-frame-test"))
}
let runtime = MacNodeRuntime(makeMainActorServices: { services })
let response = await self.invoke(
runtime, "req-fit", MacNodeScreenCommand.snapshot.rawValue, nodeId: "node-fit")
#expect(response.ok == true)
let payloadJSON = try #require(response.payloadJSON)
let projected = try MacNodeRuntime.projectedOuterFrameBytes(
forPayloadJSON: payloadJSON,
requestId: "req-fit",
nodeId: "node-fit")
#expect(projected < 25 * 1024 * 1024)
}
@Test func `projected outer frame bytes accounts for dynamic node id escaping`() throws {
let inner = "{\"format\":\"png\",\"note\":\"\u{0001}\u{0002}\n\t\\\"raw\\\"\",\"width\":1,\"height\":1,\"capturedAtMs\":0}"
let projected = try MacNodeRuntime.projectedOuterFrameBytes(
forPayloadJSON: inner,
requestId: "req-control",
nodeId: "node-\u{0001}\u{0002}\u{0003}\n\t-id")
struct Frame: Encodable {
let type = "req"
let id = "00000000-0000-0000-0000-000000000000"
let method = "node.invoke.result"
let params: Params
struct Params: Encodable {
let id: String
let nodeId: String
let ok: Bool
let payloadJSON: String
}
}
let serialized = try JSONEncoder().encode(Frame(params: Frame.Params(
id: "req-control",
nodeId: "node-\u{0001}\u{0002}\u{0003}\n\t-id",
ok: true,
payloadJSON: inner)))
#expect(projected == serialized.count)
let controlHeavyNodeId = String(repeating: "\u{0001}", count: 5 * 1024 * 1024)
let controlHeavyProjection = try MacNodeRuntime.projectedOuterFrameBytes(
forPayloadJSON: "{}",
requestId: "req-control",
nodeId: controlHeavyNodeId)
#expect(controlHeavyProjection > 25 * 1024 * 1024)
}
}