Revert "iOS: align node permissions and notifications"
This reverts commit b17e6fdd07.
This commit is contained in:
parent
ed65131c1c
commit
821ed35be1
5 changed files with 8 additions and 290 deletions
|
|
@ -1,39 +1,14 @@
|
||||||
import OpenClawKit
|
import OpenClawKit
|
||||||
import AVFoundation
|
|
||||||
import CoreLocation
|
|
||||||
import Darwin
|
import Darwin
|
||||||
import Foundation
|
import Foundation
|
||||||
import Network
|
import Network
|
||||||
import Observation
|
import Observation
|
||||||
import ReplayKit
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable
|
@Observable
|
||||||
final class GatewayConnectionController {
|
final class GatewayConnectionController {
|
||||||
struct PermissionStatusProvider: Sendable {
|
|
||||||
var cameraStatus: @Sendable () -> AVAuthorizationStatus
|
|
||||||
var microphoneStatus: @Sendable () -> AVAuthorizationStatus
|
|
||||||
var locationStatus: @Sendable () -> CLAuthorizationStatus
|
|
||||||
var locationServicesEnabled: @Sendable () -> Bool
|
|
||||||
var screenRecordingAvailable: @Sendable () -> Bool
|
|
||||||
|
|
||||||
static func live() -> PermissionStatusProvider {
|
|
||||||
PermissionStatusProvider(
|
|
||||||
cameraStatus: { AVCaptureDevice.authorizationStatus(for: .video) },
|
|
||||||
microphoneStatus: { AVCaptureDevice.authorizationStatus(for: .audio) },
|
|
||||||
locationStatus: {
|
|
||||||
if #available(iOS 14.0, *) {
|
|
||||||
return CLLocationManager.authorizationStatus()
|
|
||||||
}
|
|
||||||
return CLLocationManager().authorizationStatus
|
|
||||||
},
|
|
||||||
locationServicesEnabled: { CLLocationManager.locationServicesEnabled() },
|
|
||||||
screenRecordingAvailable: { RPScreenRecorder.shared().isAvailable })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private(set) var gateways: [GatewayDiscoveryModel.DiscoveredGateway] = []
|
private(set) var gateways: [GatewayDiscoveryModel.DiscoveredGateway] = []
|
||||||
private(set) var discoveryStatusText: String = "Idle"
|
private(set) var discoveryStatusText: String = "Idle"
|
||||||
private(set) var discoveryDebugLog: [GatewayDiscoveryModel.DebugLogEntry] = []
|
private(set) var discoveryDebugLog: [GatewayDiscoveryModel.DebugLogEntry] = []
|
||||||
|
|
@ -41,15 +16,9 @@ final class GatewayConnectionController {
|
||||||
private let discovery = GatewayDiscoveryModel()
|
private let discovery = GatewayDiscoveryModel()
|
||||||
private weak var appModel: NodeAppModel?
|
private weak var appModel: NodeAppModel?
|
||||||
private var didAutoConnect = false
|
private var didAutoConnect = false
|
||||||
private let permissionProvider: PermissionStatusProvider
|
|
||||||
|
|
||||||
init(
|
init(appModel: NodeAppModel, startDiscovery: Bool = true) {
|
||||||
appModel: NodeAppModel,
|
|
||||||
startDiscovery: Bool = true,
|
|
||||||
permissionProvider: PermissionStatusProvider = PermissionStatusProvider.live())
|
|
||||||
{
|
|
||||||
self.appModel = appModel
|
self.appModel = appModel
|
||||||
self.permissionProvider = permissionProvider
|
|
||||||
|
|
||||||
GatewaySettingsStore.bootstrapPersistence()
|
GatewaySettingsStore.bootstrapPersistence()
|
||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
|
|
@ -313,7 +282,7 @@ final class GatewayConnectionController {
|
||||||
scopes: [],
|
scopes: [],
|
||||||
caps: self.currentCaps(),
|
caps: self.currentCaps(),
|
||||||
commands: self.currentCommands(),
|
commands: self.currentCommands(),
|
||||||
permissions: self.currentPermissions(),
|
permissions: [:],
|
||||||
clientId: "openclaw-ios",
|
clientId: "openclaw-ios",
|
||||||
clientMode: "node",
|
clientMode: "node",
|
||||||
clientDisplayName: displayName)
|
clientDisplayName: displayName)
|
||||||
|
|
@ -366,6 +335,10 @@ final class GatewayConnectionController {
|
||||||
OpenClawCanvasA2UICommand.reset.rawValue,
|
OpenClawCanvasA2UICommand.reset.rawValue,
|
||||||
OpenClawScreenCommand.record.rawValue,
|
OpenClawScreenCommand.record.rawValue,
|
||||||
OpenClawSystemCommand.notify.rawValue,
|
OpenClawSystemCommand.notify.rawValue,
|
||||||
|
OpenClawSystemCommand.which.rawValue,
|
||||||
|
OpenClawSystemCommand.run.rawValue,
|
||||||
|
OpenClawSystemCommand.execApprovalsGet.rawValue,
|
||||||
|
OpenClawSystemCommand.execApprovalsSet.rawValue,
|
||||||
]
|
]
|
||||||
|
|
||||||
let caps = Set(self.currentCaps())
|
let caps = Set(self.currentCaps())
|
||||||
|
|
@ -381,32 +354,6 @@ final class GatewayConnectionController {
|
||||||
return commands
|
return commands
|
||||||
}
|
}
|
||||||
|
|
||||||
private func currentPermissions() -> [String: Bool] {
|
|
||||||
let camera = self.permissionProvider.cameraStatus()
|
|
||||||
let microphone = self.permissionProvider.microphoneStatus()
|
|
||||||
let locationStatus = self.permissionProvider.locationStatus()
|
|
||||||
let locationEnabled = self.permissionProvider.locationServicesEnabled()
|
|
||||||
let screenRecordingAvailable = self.permissionProvider.screenRecordingAvailable()
|
|
||||||
|
|
||||||
return [
|
|
||||||
"camera": camera == .authorized,
|
|
||||||
"microphone": microphone == .authorized,
|
|
||||||
"location": locationEnabled && Self.isLocationAuthorized(status: locationStatus),
|
|
||||||
"screenRecording": screenRecordingAvailable,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func isLocationAuthorized(status: CLAuthorizationStatus) -> Bool {
|
|
||||||
switch status {
|
|
||||||
case .authorizedAlways, .authorizedWhenInUse:
|
|
||||||
return true
|
|
||||||
case .authorized:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func platformString() -> String {
|
private func platformString() -> String {
|
||||||
let v = ProcessInfo.processInfo.operatingSystemVersion
|
let v = ProcessInfo.processInfo.operatingSystemVersion
|
||||||
let name = switch UIDevice.current.userInterfaceIdiom {
|
let name = switch UIDevice.current.userInterfaceIdiom {
|
||||||
|
|
@ -460,10 +407,6 @@ extension GatewayConnectionController {
|
||||||
self.currentCommands()
|
self.currentCommands()
|
||||||
}
|
}
|
||||||
|
|
||||||
func _test_currentPermissions() -> [String: Bool] {
|
|
||||||
self.currentPermissions()
|
|
||||||
}
|
|
||||||
|
|
||||||
func _test_platformString() -> String {
|
func _test_platformString() -> String {
|
||||||
self.platformString()
|
self.platformString()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,63 +3,6 @@ import Network
|
||||||
import Observation
|
import Observation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
import UserNotifications
|
|
||||||
|
|
||||||
enum NotificationAuthorizationStatus: Sendable {
|
|
||||||
case notDetermined
|
|
||||||
case denied
|
|
||||||
case authorized
|
|
||||||
case provisional
|
|
||||||
case ephemeral
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol NotificationCentering: Sendable {
|
|
||||||
func authorizationStatus() async -> NotificationAuthorizationStatus
|
|
||||||
func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool
|
|
||||||
func add(_ request: UNNotificationRequest) async throws
|
|
||||||
}
|
|
||||||
|
|
||||||
struct LiveNotificationCenter: NotificationCentering, @unchecked Sendable {
|
|
||||||
private let center: UNUserNotificationCenter
|
|
||||||
|
|
||||||
init(center: UNUserNotificationCenter = .current()) {
|
|
||||||
self.center = center
|
|
||||||
}
|
|
||||||
|
|
||||||
func authorizationStatus() async -> NotificationAuthorizationStatus {
|
|
||||||
let settings = await self.center.notificationSettings()
|
|
||||||
return switch settings.authorizationStatus {
|
|
||||||
case .authorized:
|
|
||||||
.authorized
|
|
||||||
case .provisional:
|
|
||||||
.provisional
|
|
||||||
case .ephemeral:
|
|
||||||
.ephemeral
|
|
||||||
case .denied:
|
|
||||||
.denied
|
|
||||||
case .notDetermined:
|
|
||||||
.notDetermined
|
|
||||||
@unknown default:
|
|
||||||
.denied
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool {
|
|
||||||
try await self.center.requestAuthorization(options: options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func add(_ request: UNNotificationRequest) async throws {
|
|
||||||
try await withCheckedThrowingContinuation { cont in
|
|
||||||
self.center.add(request) { error in
|
|
||||||
if let error {
|
|
||||||
cont.resume(throwing: error)
|
|
||||||
} else {
|
|
||||||
cont.resume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable
|
@Observable
|
||||||
|
|
@ -85,7 +28,6 @@ final class NodeAppModel {
|
||||||
private let gateway = GatewayNodeSession()
|
private let gateway = GatewayNodeSession()
|
||||||
private var gatewayTask: Task<Void, Never>?
|
private var gatewayTask: Task<Void, Never>?
|
||||||
private var voiceWakeSyncTask: Task<Void, Never>?
|
private var voiceWakeSyncTask: Task<Void, Never>?
|
||||||
private let notificationCenter: NotificationCentering
|
|
||||||
@ObservationIgnored private var cameraHUDDismissTask: Task<Void, Never>?
|
@ObservationIgnored private var cameraHUDDismissTask: Task<Void, Never>?
|
||||||
let voiceWake = VoiceWakeManager()
|
let voiceWake = VoiceWakeManager()
|
||||||
let talkMode = TalkModeManager()
|
let talkMode = TalkModeManager()
|
||||||
|
|
@ -100,8 +42,7 @@ final class NodeAppModel {
|
||||||
var cameraFlashNonce: Int = 0
|
var cameraFlashNonce: Int = 0
|
||||||
var screenRecordActive: Bool = false
|
var screenRecordActive: Bool = false
|
||||||
|
|
||||||
init(notificationCenter: NotificationCentering = LiveNotificationCenter()) {
|
init() {
|
||||||
self.notificationCenter = notificationCenter
|
|
||||||
self.voiceWake.configure { [weak self] cmd in
|
self.voiceWake.configure { [weak self] cmd in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
let sessionKey = await MainActor.run { self.mainSessionKey }
|
let sessionKey = await MainActor.run { self.mainSessionKey }
|
||||||
|
|
@ -601,14 +542,12 @@ final class NodeAppModel {
|
||||||
return try await self.handleCameraInvoke(req)
|
return try await self.handleCameraInvoke(req)
|
||||||
case OpenClawScreenCommand.record.rawValue:
|
case OpenClawScreenCommand.record.rawValue:
|
||||||
return try await self.handleScreenRecordInvoke(req)
|
return try await self.handleScreenRecordInvoke(req)
|
||||||
case OpenClawSystemCommand.notify.rawValue:
|
|
||||||
return try await self.handleSystemNotify(req)
|
|
||||||
default:
|
default:
|
||||||
return BridgeInvokeResponse(
|
return BridgeInvokeResponse(
|
||||||
id: req.id,
|
id: req.id,
|
||||||
ok: false,
|
ok: false,
|
||||||
error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command"))
|
error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command"))
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if command.hasPrefix("camera.") {
|
if command.hasPrefix("camera.") {
|
||||||
let text = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
|
let text = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
|
||||||
|
|
@ -689,7 +628,6 @@ final class NodeAppModel {
|
||||||
case OpenClawCanvasCommand.present.rawValue:
|
case OpenClawCanvasCommand.present.rawValue:
|
||||||
let params = (try? Self.decodeParams(OpenClawCanvasPresentParams.self, from: req.paramsJSON)) ??
|
let params = (try? Self.decodeParams(OpenClawCanvasPresentParams.self, from: req.paramsJSON)) ??
|
||||||
OpenClawCanvasPresentParams()
|
OpenClawCanvasPresentParams()
|
||||||
// iOS ignores placement params (canvas presents full-screen).
|
|
||||||
let url = params.url?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
let url = params.url?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
if url.isEmpty {
|
if url.isEmpty {
|
||||||
self.screen.showDefaultCanvas()
|
self.screen.showDefaultCanvas()
|
||||||
|
|
@ -698,7 +636,6 @@ final class NodeAppModel {
|
||||||
}
|
}
|
||||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
return BridgeInvokeResponse(id: req.id, ok: true)
|
||||||
case OpenClawCanvasCommand.hide.rawValue:
|
case OpenClawCanvasCommand.hide.rawValue:
|
||||||
self.showLocalCanvasOnDisconnect()
|
|
||||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
return BridgeInvokeResponse(id: req.id, ok: true)
|
||||||
case OpenClawCanvasCommand.navigate.rawValue:
|
case OpenClawCanvasCommand.navigate.rawValue:
|
||||||
let params = try Self.decodeParams(OpenClawCanvasNavigateParams.self, from: req.paramsJSON)
|
let params = try Self.decodeParams(OpenClawCanvasNavigateParams.self, from: req.paramsJSON)
|
||||||
|
|
@ -922,58 +859,6 @@ final class NodeAppModel {
|
||||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleSystemNotify(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
|
||||||
let params = try Self.decodeParams(OpenClawSystemNotifyParams.self, from: req.paramsJSON)
|
|
||||||
let status = await self.notificationCenter.authorizationStatus()
|
|
||||||
let authorized: Bool
|
|
||||||
switch status {
|
|
||||||
case .authorized, .provisional, .ephemeral:
|
|
||||||
authorized = true
|
|
||||||
case .notDetermined:
|
|
||||||
authorized = (try await self.notificationCenter
|
|
||||||
.requestAuthorization(options: [.alert, .sound, .badge]))
|
|
||||||
case .denied:
|
|
||||||
authorized = false
|
|
||||||
}
|
|
||||||
|
|
||||||
guard authorized else {
|
|
||||||
return BridgeInvokeResponse(
|
|
||||||
id: req.id,
|
|
||||||
ok: false,
|
|
||||||
error: OpenClawNodeError(
|
|
||||||
code: .unavailable,
|
|
||||||
message: "NOTIFICATION_PERMISSION_REQUIRED: enable Notifications in Settings"))
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
|
||||||
content.title = params.title
|
|
||||||
content.body = params.body
|
|
||||||
let sound = params.sound?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
||||||
if sound.isEmpty {
|
|
||||||
content.sound = .default
|
|
||||||
} else {
|
|
||||||
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: sound))
|
|
||||||
}
|
|
||||||
if let priority = params.priority {
|
|
||||||
switch priority {
|
|
||||||
case .passive:
|
|
||||||
content.interruptionLevel = .passive
|
|
||||||
case .active:
|
|
||||||
content.interruptionLevel = .active
|
|
||||||
case .timeSensitive:
|
|
||||||
content.interruptionLevel = .timeSensitive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
|
|
||||||
let request = UNNotificationRequest(
|
|
||||||
identifier: UUID().uuidString,
|
|
||||||
content: content,
|
|
||||||
trigger: trigger)
|
|
||||||
try await self.notificationCenter.add(request)
|
|
||||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension NodeAppModel {
|
private extension NodeAppModel {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import OpenClawKit
|
import OpenClawKit
|
||||||
import AVFoundation
|
|
||||||
import CoreLocation
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Testing
|
import Testing
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
@ -78,41 +76,4 @@ private func withUserDefaults<T>(_ updates: [String: Any?], _ body: () throws ->
|
||||||
#expect(commands.contains(OpenClawLocationCommand.get.rawValue))
|
#expect(commands.contains(OpenClawLocationCommand.get.rawValue))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test @MainActor func currentCommandsExcludeSystemExecButKeepNotify() {
|
|
||||||
withUserDefaults([
|
|
||||||
"node.instanceId": "ios-test",
|
|
||||||
]) {
|
|
||||||
let appModel = NodeAppModel()
|
|
||||||
let controller = GatewayConnectionController(appModel: appModel, startDiscovery: false)
|
|
||||||
let commands = Set(controller._test_currentCommands())
|
|
||||||
|
|
||||||
#expect(commands.contains(OpenClawSystemCommand.notify.rawValue))
|
|
||||||
#expect(commands.contains(OpenClawSystemCommand.run.rawValue) == false)
|
|
||||||
#expect(commands.contains(OpenClawSystemCommand.which.rawValue) == false)
|
|
||||||
#expect(commands.contains(OpenClawSystemCommand.execApprovalsGet.rawValue) == false)
|
|
||||||
#expect(commands.contains(OpenClawSystemCommand.execApprovalsSet.rawValue) == false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test @MainActor func currentPermissionsIncludeExpectedKeys() {
|
|
||||||
let provider = GatewayConnectionController.PermissionStatusProvider(
|
|
||||||
cameraStatus: { .authorized },
|
|
||||||
microphoneStatus: { .denied },
|
|
||||||
locationStatus: { .authorizedWhenInUse },
|
|
||||||
locationServicesEnabled: { true },
|
|
||||||
screenRecordingAvailable: { false })
|
|
||||||
|
|
||||||
let appModel = NodeAppModel()
|
|
||||||
let controller = GatewayConnectionController(
|
|
||||||
appModel: appModel,
|
|
||||||
startDiscovery: false,
|
|
||||||
permissionProvider: provider)
|
|
||||||
let permissions = controller._test_currentPermissions()
|
|
||||||
|
|
||||||
#expect(permissions["camera"] == true)
|
|
||||||
#expect(permissions["microphone"] == false)
|
|
||||||
#expect(permissions["location"] == true)
|
|
||||||
#expect(permissions["screenRecording"] == false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -124,11 +124,6 @@ private func withUserDefaults<T>(_ updates: [String: Any?], _ body: () throws ->
|
||||||
let payloadData = try #require(evalRes.payloadJSON?.data(using: .utf8))
|
let payloadData = try #require(evalRes.payloadJSON?.data(using: .utf8))
|
||||||
let payload = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any]
|
let payload = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any]
|
||||||
#expect(payload?["result"] as? String == "2")
|
#expect(payload?["result"] as? String == "2")
|
||||||
|
|
||||||
let hide = BridgeInvokeRequest(id: "hide", command: OpenClawCanvasCommand.hide.rawValue)
|
|
||||||
let hideRes = await appModel._test_handleInvoke(hide)
|
|
||||||
#expect(hideRes.ok == true)
|
|
||||||
#expect(appModel.screen.urlString.isEmpty)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test @MainActor func handleInvokeA2UICommandsFailWhenHostMissing() async throws {
|
@Test @MainActor func handleInvokeA2UICommandsFailWhenHostMissing() async throws {
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import OpenClawKit
|
|
||||||
import Foundation
|
|
||||||
import Testing
|
|
||||||
import UserNotifications
|
|
||||||
@testable import OpenClaw
|
|
||||||
|
|
||||||
actor TestNotificationCenter: NotificationCentering {
|
|
||||||
private var status: NotificationAuthorizationStatus
|
|
||||||
private let requestResult: Bool
|
|
||||||
private var requestedAuthorization: Bool = false
|
|
||||||
private var storedRequests: [UNNotificationRequest] = []
|
|
||||||
|
|
||||||
init(status: NotificationAuthorizationStatus, requestResult: Bool) {
|
|
||||||
self.status = status
|
|
||||||
self.requestResult = requestResult
|
|
||||||
}
|
|
||||||
|
|
||||||
func authorizationStatus() async -> NotificationAuthorizationStatus {
|
|
||||||
status
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestAuthorization(options _: UNAuthorizationOptions) async throws -> Bool {
|
|
||||||
self.requestedAuthorization = true
|
|
||||||
if self.requestResult {
|
|
||||||
self.status = .authorized
|
|
||||||
}
|
|
||||||
return self.requestResult
|
|
||||||
}
|
|
||||||
|
|
||||||
func add(_ request: UNNotificationRequest) async throws {
|
|
||||||
self.storedRequests.append(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func didRequestAuthorization() async -> Bool {
|
|
||||||
requestedAuthorization
|
|
||||||
}
|
|
||||||
|
|
||||||
func requests() async -> [UNNotificationRequest] {
|
|
||||||
storedRequests
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suite(.serialized) struct NodeAppModelNotifyTests {
|
|
||||||
@Test @MainActor func handleSystemNotifyRequestsPermissionAndAddsNotification() async throws {
|
|
||||||
let center = TestNotificationCenter(status: .notDetermined, requestResult: true)
|
|
||||||
let appModel = NodeAppModel(notificationCenter: center)
|
|
||||||
|
|
||||||
let params = OpenClawSystemNotifyParams(title: "Hello", body: "World")
|
|
||||||
let data = try JSONEncoder().encode(params)
|
|
||||||
let json = String(decoding: data, as: UTF8.self)
|
|
||||||
|
|
||||||
let req = BridgeInvokeRequest(
|
|
||||||
id: "notify",
|
|
||||||
command: OpenClawSystemCommand.notify.rawValue,
|
|
||||||
paramsJSON: json)
|
|
||||||
|
|
||||||
let res = await appModel._test_handleInvoke(req)
|
|
||||||
#expect(res.ok == true)
|
|
||||||
#expect(await center.didRequestAuthorization() == true)
|
|
||||||
|
|
||||||
let requests = await center.requests()
|
|
||||||
#expect(requests.count == 1)
|
|
||||||
#expect(requests.first?.content.title == "Hello")
|
|
||||||
#expect(requests.first?.content.body == "World")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue