| import OpenClawKit |
| import Foundation |
| import Testing |
| import UIKit |
| @testable import OpenClaw |
|
|
| private func withUserDefaults<T>(_ updates: [String: Any?], _ body: () throws -> T) rethrows -> T { |
| let defaults = UserDefaults.standard |
| var snapshot: [String: Any?] = [:] |
| for key in updates.keys { |
| snapshot[key] = defaults.object(forKey: key) |
| } |
| for (key, value) in updates { |
| if let value { |
| defaults.set(value, forKey: key) |
| } else { |
| defaults.removeObject(forKey: key) |
| } |
| } |
| defer { |
| for (key, value) in snapshot { |
| if let value { |
| defaults.set(value, forKey: key) |
| } else { |
| defaults.removeObject(forKey: key) |
| } |
| } |
| } |
| return try body() |
| } |
|
|
| @Suite(.serialized) struct NodeAppModelInvokeTests { |
| @Test @MainActor func decodeParamsFailsWithoutJSON() { |
| #expect(throws: Error.self) { |
| _ = try NodeAppModel._test_decodeParams(OpenClawCanvasNavigateParams.self, from: nil) |
| } |
| } |
|
|
| @Test @MainActor func encodePayloadEmitsJSON() throws { |
| struct Payload: Codable, Equatable { |
| var value: String |
| } |
| let json = try NodeAppModel._test_encodePayload(Payload(value: "ok")) |
| #expect(json.contains("\"value\"")) |
| } |
|
|
| @Test @MainActor func handleInvokeRejectsBackgroundCommands() async { |
| let appModel = NodeAppModel() |
| appModel.setScenePhase(.background) |
|
|
| let req = BridgeInvokeRequest(id: "bg", command: OpenClawCanvasCommand.present.rawValue) |
| let res = await appModel._test_handleInvoke(req) |
| #expect(res.ok == false) |
| #expect(res.error?.code == .backgroundUnavailable) |
| } |
|
|
| @Test @MainActor func handleInvokeRejectsCameraWhenDisabled() async { |
| let appModel = NodeAppModel() |
| let req = BridgeInvokeRequest(id: "cam", command: OpenClawCameraCommand.snap.rawValue) |
|
|
| let defaults = UserDefaults.standard |
| let key = "camera.enabled" |
| let previous = defaults.object(forKey: key) |
| defaults.set(false, forKey: key) |
| defer { |
| if let previous { |
| defaults.set(previous, forKey: key) |
| } else { |
| defaults.removeObject(forKey: key) |
| } |
| } |
|
|
| let res = await appModel._test_handleInvoke(req) |
| #expect(res.ok == false) |
| #expect(res.error?.code == .unavailable) |
| #expect(res.error?.message.contains("CAMERA_DISABLED") == true) |
| } |
|
|
| @Test @MainActor func handleInvokeRejectsInvalidScreenFormat() async { |
| let appModel = NodeAppModel() |
| let params = OpenClawScreenRecordParams(format: "gif") |
| let data = try? JSONEncoder().encode(params) |
| let json = data.flatMap { String(data: $0, encoding: .utf8) } |
|
|
| let req = BridgeInvokeRequest( |
| id: "screen", |
| command: OpenClawScreenCommand.record.rawValue, |
| paramsJSON: json) |
|
|
| let res = await appModel._test_handleInvoke(req) |
| #expect(res.ok == false) |
| #expect(res.error?.message.contains("screen format must be mp4") == true) |
| } |
|
|
| @Test @MainActor func handleInvokeCanvasCommandsUpdateScreen() async throws { |
| let appModel = NodeAppModel() |
| appModel.screen.navigate(to: "http://example.com") |
|
|
| let present = BridgeInvokeRequest(id: "present", command: OpenClawCanvasCommand.present.rawValue) |
| let presentRes = await appModel._test_handleInvoke(present) |
| #expect(presentRes.ok == true) |
| #expect(appModel.screen.urlString.isEmpty) |
|
|
| let navigateParams = OpenClawCanvasNavigateParams(url: "http://localhost:7860/") |
| let navData = try JSONEncoder().encode(navigateParams) |
| let navJSON = String(decoding: navData, as: UTF8.self) |
| let navigate = BridgeInvokeRequest( |
| id: "nav", |
| command: OpenClawCanvasCommand.navigate.rawValue, |
| paramsJSON: navJSON) |
| let navRes = await appModel._test_handleInvoke(navigate) |
| #expect(navRes.ok == true) |
| #expect(appModel.screen.urlString == "http://localhost:7860/") |
|
|
| let evalParams = OpenClawCanvasEvalParams(javaScript: "1+1") |
| let evalData = try JSONEncoder().encode(evalParams) |
| let evalJSON = String(decoding: evalData, as: UTF8.self) |
| let eval = BridgeInvokeRequest( |
| id: "eval", |
| command: OpenClawCanvasCommand.evalJS.rawValue, |
| paramsJSON: evalJSON) |
| let evalRes = await appModel._test_handleInvoke(eval) |
| #expect(evalRes.ok == true) |
| let payloadData = try #require(evalRes.payloadJSON?.data(using: .utf8)) |
| let payload = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any] |
| #expect(payload?["result"] as? String == "2") |
| } |
|
|
| @Test @MainActor func handleInvokeA2UICommandsFailWhenHostMissing() async throws { |
| let appModel = NodeAppModel() |
|
|
| let reset = BridgeInvokeRequest(id: "reset", command: OpenClawCanvasA2UICommand.reset.rawValue) |
| let resetRes = await appModel._test_handleInvoke(reset) |
| #expect(resetRes.ok == false) |
| #expect(resetRes.error?.message.contains("A2UI_HOST_NOT_CONFIGURED") == true) |
|
|
| let jsonl = "{\"beginRendering\":{}}" |
| let pushParams = OpenClawCanvasA2UIPushJSONLParams(jsonl: jsonl) |
| let pushData = try JSONEncoder().encode(pushParams) |
| let pushJSON = String(decoding: pushData, as: UTF8.self) |
| let push = BridgeInvokeRequest( |
| id: "push", |
| command: OpenClawCanvasA2UICommand.pushJSONL.rawValue, |
| paramsJSON: pushJSON) |
| let pushRes = await appModel._test_handleInvoke(push) |
| #expect(pushRes.ok == false) |
| #expect(pushRes.error?.message.contains("A2UI_HOST_NOT_CONFIGURED") == true) |
| } |
|
|
| @Test @MainActor func handleInvokeUnknownCommandReturnsInvalidRequest() async { |
| let appModel = NodeAppModel() |
| let req = BridgeInvokeRequest(id: "unknown", command: "nope") |
| let res = await appModel._test_handleInvoke(req) |
| #expect(res.ok == false) |
| #expect(res.error?.code == .invalidRequest) |
| } |
|
|
| @Test @MainActor func handleDeepLinkSetsErrorWhenNotConnected() async { |
| let appModel = NodeAppModel() |
| let url = URL(string: "openclaw://agent?message=hello")! |
| await appModel.handleDeepLink(url: url) |
| #expect(appModel.screen.errorText?.contains("Gateway not connected") == true) |
| } |
|
|
| @Test @MainActor func handleDeepLinkRejectsOversizedMessage() async { |
| let appModel = NodeAppModel() |
| let msg = String(repeating: "a", count: 20001) |
| let url = URL(string: "openclaw://agent?message=\(msg)")! |
| await appModel.handleDeepLink(url: url) |
| #expect(appModel.screen.errorText?.contains("Deep link too large") == true) |
| } |
|
|
| @Test @MainActor func sendVoiceTranscriptThrowsWhenGatewayOffline() async { |
| let appModel = NodeAppModel() |
| await #expect(throws: Error.self) { |
| try await appModel.sendVoiceTranscript(text: "hello", sessionKey: "main") |
| } |
| } |
|
|
| @Test @MainActor func canvasA2UIActionDispatchesStatus() async { |
| let appModel = NodeAppModel() |
| let body: [String: Any] = [ |
| "userAction": [ |
| "name": "tap", |
| "id": "action-1", |
| "surfaceId": "main", |
| "sourceComponentId": "button-1", |
| "context": ["value": "ok"], |
| ], |
| ] |
| await appModel._test_handleCanvasA2UIAction(body: body) |
| #expect(appModel.screen.urlString.isEmpty) |
| } |
| } |
|
|