mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 17:03:59 +00:00
* fix(ios): defer QR pairing after scanner dismissal * fix(ios): process QR pairing after scanner dismissal * fix(ios): harden QR scanner handoff * fix(ios): give QR scanner dismissal more time * fix(ios): keep onboarding open for QR trust prompt * fix(ios): keep QR trust prompt owned by onboarding * fix(ios): recover operator pairing after QR bootstrap * fix(ios): cancel stale QR scanner handoffs Co-authored-by: PollyBot13 <pollybot13@gmail.com> * fix(ios): defer QR setup until onboarding closes * fix(ios): keep QR setup links with visible settings * fix(ios): consume setup links during onboarding * fix(ios): handle setup links during onboarding launch * fix(ios): route setup links through active onboarding * fix(ios): harden QR gateway handoff * fix(ios): cancel superseded gateway attempts * fix(ios): serialize scanner result delivery * fix(ios): prevent stale gateway reconnects * fix(ios): serialize gateway target handoff * fix(ios): disable stale gateway relaunch route * fix(ios): await staged bootstrap reset * test(ios): bound gateway reset handoff * fix(ios): preserve explicit gateway handoff * fix(ios): harden gateway lifecycle ownership * chore(ios): sync native i18n inventory * test(ios): align gateway ownership assertions * refactor(ios): remove superseded gateway helpers * fix(ios): keep gateway auth route scoped * fix(ios): restore gateway target review state * fix(protocol): refresh Swift plugin approval model * test(ios): isolate state directory overrides * fix(ios): preserve watch alerts across gateway switches * fix(ios): bind deferred work to gateway ownership * docs(changelog): credit iOS gateway handoff fix * chore(i18n): sync native app inventory * test(ios): remove unused Watch approval hooks --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
167 lines
6.2 KiB
Swift
167 lines
6.2 KiB
Swift
import Foundation
|
|
import Testing
|
|
import UserNotifications
|
|
@testable import OpenClaw
|
|
|
|
private final class MockNotificationCenter: NotificationCentering, @unchecked Sendable {
|
|
var authorization: NotificationAuthorizationStatus = .authorized
|
|
var addedRequests: [UNNotificationRequest] = []
|
|
var pendingRemovedIdentifiers: [[String]] = []
|
|
var deliveredRemovedIdentifiers: [[String]] = []
|
|
var delivered: [NotificationSnapshot] = []
|
|
|
|
func authorizationStatus() async -> NotificationAuthorizationStatus {
|
|
self.authorization
|
|
}
|
|
|
|
func add(_ request: UNNotificationRequest) async throws {
|
|
self.addedRequests.append(request)
|
|
}
|
|
|
|
func removePendingNotificationRequests(withIdentifiers identifiers: [String]) async {
|
|
self.pendingRemovedIdentifiers.append(identifiers)
|
|
}
|
|
|
|
func removeDeliveredNotifications(withIdentifiers identifiers: [String]) async {
|
|
self.deliveredRemovedIdentifiers.append(identifiers)
|
|
}
|
|
|
|
func deliveredNotifications() async -> [NotificationSnapshot] {
|
|
self.delivered
|
|
}
|
|
}
|
|
|
|
@Suite(.serialized) struct ExecApprovalNotificationBridgeTests {
|
|
@Test func `parse prompt maps default notification tap`() {
|
|
let prompt = ExecApprovalNotificationBridge.parsePrompt(
|
|
actionIdentifier: UNNotificationDefaultActionIdentifier,
|
|
userInfo: [
|
|
"openclaw": [
|
|
"kind": ExecApprovalNotificationBridge.requestedKind,
|
|
"approvalId": "approval-123",
|
|
"gatewayDeviceId": "gateway-a",
|
|
],
|
|
])
|
|
|
|
#expect(prompt == ExecApprovalNotificationPrompt(
|
|
approvalId: "approval-123",
|
|
gatewayDeviceId: "gateway-a"))
|
|
}
|
|
|
|
@Test func `parse prompt maps review action`() {
|
|
let prompt = ExecApprovalNotificationBridge.parsePrompt(
|
|
actionIdentifier: ExecApprovalNotificationBridge.reviewActionIdentifier,
|
|
userInfo: [
|
|
"openclaw": [
|
|
"kind": ExecApprovalNotificationBridge.requestedKind,
|
|
"approvalId": "approval-456",
|
|
"gatewayDeviceId": "gateway-b",
|
|
],
|
|
])
|
|
|
|
#expect(prompt == ExecApprovalNotificationPrompt(
|
|
approvalId: "approval-456",
|
|
gatewayDeviceId: "gateway-b"))
|
|
}
|
|
|
|
@Test func `parse prompt ignores unexpected action identifiers`() {
|
|
let prompt = ExecApprovalNotificationBridge.parsePrompt(
|
|
actionIdentifier: "openclaw.exec-approval.allow-once",
|
|
userInfo: [
|
|
"openclaw": [
|
|
"kind": ExecApprovalNotificationBridge.requestedKind,
|
|
"approvalId": "approval-789",
|
|
],
|
|
])
|
|
|
|
#expect(prompt == nil)
|
|
}
|
|
|
|
@Test @MainActor func `handle resolved push removes matching notifications`() async {
|
|
let center = MockNotificationCenter()
|
|
center.delivered = [
|
|
NotificationSnapshot(
|
|
identifier: "remote-approval-1",
|
|
userInfo: [
|
|
"openclaw": [
|
|
"kind": ExecApprovalNotificationBridge.requestedKind,
|
|
"approvalId": "approval-123",
|
|
"gatewayDeviceId": "gateway-a",
|
|
],
|
|
]),
|
|
NotificationSnapshot(
|
|
identifier: "remote-other",
|
|
userInfo: [
|
|
"openclaw": [
|
|
"kind": ExecApprovalNotificationBridge.requestedKind,
|
|
"approvalId": "approval-123",
|
|
"gatewayDeviceId": "gateway-b",
|
|
],
|
|
]),
|
|
]
|
|
|
|
let push = ExecApprovalNotificationPrompt(
|
|
approvalId: "approval-123",
|
|
gatewayDeviceId: "gateway-a")
|
|
await ExecApprovalNotificationBridge.removeNotifications(
|
|
for: push,
|
|
notificationCenter: center)
|
|
|
|
#expect(center.pendingRemovedIdentifiers == [["exec.approval.gateway-a.approval-123"]])
|
|
#expect(center.deliveredRemovedIdentifiers == [["remote-approval-1"]])
|
|
}
|
|
|
|
@Test func `legacy ownerless approval pushes remain parseable for authenticated route validation`() {
|
|
let userInfo: [AnyHashable: Any] = [
|
|
"openclaw": [
|
|
"kind": ExecApprovalNotificationBridge.requestedKind,
|
|
"approvalId": "approval-ownerless",
|
|
],
|
|
]
|
|
|
|
#expect(ExecApprovalNotificationBridge.parseRequestedPush(userInfo: userInfo) ==
|
|
ExecApprovalNotificationPrompt(
|
|
approvalId: "approval-ownerless",
|
|
gatewayDeviceId: nil))
|
|
#expect(ExecApprovalNotificationBridge.shouldPresentNotification(userInfo: userInfo))
|
|
}
|
|
|
|
@Test @MainActor func `validated cleanup removes legacy ownerless alerts but preserves other owners`() async {
|
|
let center = MockNotificationCenter()
|
|
center.delivered = [
|
|
NotificationSnapshot(
|
|
identifier: "legacy-ownerless",
|
|
userInfo: [
|
|
"openclaw": [
|
|
"kind": ExecApprovalNotificationBridge.requestedKind,
|
|
"approvalId": "approval-shared",
|
|
],
|
|
]),
|
|
NotificationSnapshot(
|
|
identifier: "other-owner",
|
|
userInfo: [
|
|
"openclaw": [
|
|
"kind": ExecApprovalNotificationBridge.requestedKind,
|
|
"approvalId": "approval-shared",
|
|
"gatewayDeviceId": "gateway-b",
|
|
],
|
|
]),
|
|
]
|
|
let push = ExecApprovalNotificationPrompt(
|
|
approvalId: "approval-shared",
|
|
gatewayDeviceId: "gateway-a")
|
|
|
|
await ExecApprovalNotificationBridge.removeNotifications(
|
|
for: push,
|
|
notificationCenter: center,
|
|
includingLegacyOwnerless: true)
|
|
|
|
#expect(center.pendingRemovedIdentifiers == [[
|
|
"exec.approval.gateway-a.approval-shared",
|
|
"exec.approval.approval-shared",
|
|
"exec.approval.legacy.approval-shared",
|
|
]])
|
|
#expect(center.deliveredRemovedIdentifiers == [["legacy-ownerless"]])
|
|
}
|
|
}
|