Files
openclaw/apps/ios/Tests/QRScannerResultHandoffTests.swift
PollyBot13 a320f775f0 fix(ios): defer QR pairing after scanner dismissal (#99572)
* 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>
2026-07-05 12:31:53 -07:00

142 lines
4.9 KiB
Swift

import OpenClawKit
import Testing
@testable import OpenClaw
@MainActor
struct QRScannerResultHandoffTests {
@Test func `queued result is delivered once after dismissal`() async throws {
let handoff = QRScannerResultHandoff(settlingNanoseconds: 0)
var deliveredResult: QRScannerResult?
let scanID = handoff.beginScan()
handoff.queue(.setupCode("review-demo"), scanID: scanID)
let task = try #require(handoff.processAfterDismissal { deliveredResult = $0 })
await task.value
#expect(deliveredResult == .setupCode("review-demo"))
#expect(handoff.processAfterDismissal { _ in } == nil)
}
@Test func `cancel prevents queued delivery`() async throws {
let handoff = QRScannerResultHandoff(settlingNanoseconds: 1_000_000_000)
var deliveredResult: QRScannerResult?
let scanID = handoff.beginScan()
handoff.queue(.setupCode("review-demo"), scanID: scanID)
let task = try #require(handoff.processAfterDismissal { deliveredResult = $0 })
handoff.cancel()
await task.value
#expect(deliveredResult == nil)
}
@Test func `beginning another scan clears stale result`() {
let handoff = QRScannerResultHandoff(settlingNanoseconds: 0)
let staleScanID = handoff.beginScan()
handoff.queue(.setupCode("stale"), scanID: staleScanID)
handoff.beginScan()
#expect(handoff.processAfterDismissal { _ in } == nil)
}
@Test func `late result from cancelled scan cannot replace newer input`() async throws {
let handoff = QRScannerResultHandoff(settlingNanoseconds: 0)
let staleScanID = handoff.beginScan()
handoff.cancel()
let currentScanID = handoff.beginScan()
var deliveredResult: QRScannerResult?
#expect(!handoff.queue(.setupCode("stale"), scanID: staleScanID))
#expect(handoff.queue(.setupCode("current"), scanID: currentScanID))
let task = try #require(handoff.processAfterDismissal { deliveredResult = $0 })
await task.value
#expect(deliveredResult == .setupCode("current"))
}
@Test func `first producer claims the active scan`() async throws {
let handoff = QRScannerResultHandoff(settlingNanoseconds: 0)
let scanID = handoff.beginScan()
var deliveredResult: QRScannerResult?
#expect(handoff.queue(.setupCode("camera"), scanID: scanID))
#expect(!handoff.isActive(scanID: scanID))
#expect(!handoff.queue(.setupCode("photo"), scanID: scanID))
let task = try #require(handoff.processAfterDismissal { deliveredResult = $0 })
await task.value
#expect(deliveredResult == .setupCode("camera"))
}
}
struct GatewaySetupLinkStagingTests {
private static func link() -> GatewayConnectDeepLink {
GatewayConnectDeepLink(
host: "gateway.example.com",
port: 443,
tls: true,
bootstrapToken: "bootstrap",
token: "token",
password: "password")
}
@Test func `staged link is consumed once`() {
var staging = GatewaySetupLinkStaging()
let link = Self.link()
staging.stage(link)
#expect(staging.take() == link)
#expect(staging.take() == nil)
}
@Test func `cancel discards staged credentials`() {
var staging = GatewaySetupLinkStaging()
staging.stage(Self.link())
let cancelled = staging.cancel()
#expect(cancelled)
#expect(staging.link == nil)
let cancelledAgain = staging.cancel()
#expect(!cancelledAgain)
}
@Test func `new setup link replaces the pending candidate`() {
var staging = GatewaySetupLinkStaging()
let replacement = GatewayConnectDeepLink(
host: "replacement.example.com",
port: 8443,
tls: true,
bootstrapToken: nil,
token: nil,
password: nil)
staging.stage(Self.link())
staging.stage(replacement)
#expect(staging.take() == replacement)
}
}
@MainActor
struct GatewayPendingTargetSuppressionTests {
@Test func `new setup target cannot be released by stale scanner dismissal`() {
let appModel = NodeAppModel()
let controller = GatewayConnectionController(appModel: appModel, startDiscovery: false)
var pending = GatewayPendingTargetSuppression()
let scannerLease = controller.cancelPendingConnectionAttempts()
pending.replace(owner: .qrScanner, lease: scannerLease)
let setupLease = controller.cancelPendingConnectionAttempts()
pending.replace(owner: .setupLink, lease: setupLease)
#expect(pending.take(ifOwnedBy: .qrScanner) == nil)
let activeLease = pending.take(ifOwnedBy: .setupLink)
#expect(activeLease != nil)
if let activeLease {
controller.releaseAutoConnectSuppression(after: activeLease)
}
#expect(!controller._test_isAutoConnectSuppressed())
}
}