mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-16 13:51:37 +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>
142 lines
4.9 KiB
Swift
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())
|
|
}
|
|
}
|