field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

commit d34c612ea36cd7e1530d67675236669c897460f9
parent 46cdc07a0a4b971ef3c081f63069a5969fecd64f
Author: triesap <tyson@radroots.org>
Date:   Sun,  9 Aug 2026 19:23:09 +0000

ios: harden physical Blossom qualification

- retain redacted runtime codes for actionable device diagnostics
- save verified media when no writable relay is configured
- keep submission controls visible above compact tab bars
- pin regenerated FFI artifacts to the descriptor ownership fix

Diffstat:
MCargo.lock | 36++++++++++++++++++------------------
MCargo.toml | 2+-
MRadroots/State/RadrootsAddStore.swift | 23+++++++++++++++++++----
MRadroots/Views/RadrootsAddView.swift | 35+++++++++++++++++++++++++++++------
MRadrootsFFI/provenance.json | 8++++----
MRadrootsFFI/source.lock | 10+++++-----
MRadrootsTests/RadrootsAddStoreTests.swift | 72+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
MRadrootsUITests/RadrootsRemoteQualificationUITests.swift | 117++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mapi/RadrootsApp.symbols.json | 2+-
Mradroots.lib.source-lock.v1.toml | 6+++---
Mscripts/xcode.sh | 25+++++++++++++++++++++++++
11 files changed, 272 insertions(+), 64 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1654,7 +1654,7 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "radroots_blossom" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "mediatype", "serde", @@ -1666,7 +1666,7 @@ dependencies = [ [[package]] name = "radroots_core" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "rust_decimal", "serde", @@ -1675,7 +1675,7 @@ dependencies = [ [[package]] name = "radroots_event" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "hex", "jiff-tzdb", @@ -1693,7 +1693,7 @@ dependencies = [ [[package]] name = "radroots_event_codec" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "hex", "radroots_blossom", @@ -1710,7 +1710,7 @@ dependencies = [ [[package]] name = "radroots_identity" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "k256", "serde", @@ -1727,7 +1727,7 @@ dependencies = [ [[package]] name = "radroots_mobile_core" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "chrono", "hex", @@ -1751,7 +1751,7 @@ dependencies = [ [[package]] name = "radroots_mobile_ffi" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "async-trait", "hex", @@ -1773,7 +1773,7 @@ dependencies = [ [[package]] name = "radroots_nostr" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "base64", "nostr", @@ -1789,7 +1789,7 @@ dependencies = [ [[package]] name = "radroots_protocol" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "serde", ] @@ -1797,7 +1797,7 @@ dependencies = [ [[package]] name = "radroots_sdk" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "radroots_blossom", "radroots_core", @@ -1822,7 +1822,7 @@ dependencies = [ [[package]] name = "radroots_secrets" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "chacha20poly1305", "sha2", @@ -1833,7 +1833,7 @@ dependencies = [ [[package]] name = "radroots_signing" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "hex", "radroots_event", @@ -1847,7 +1847,7 @@ dependencies = [ [[package]] name = "radroots_storage" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "radroots_event", "radroots_event_codec", @@ -1861,7 +1861,7 @@ dependencies = [ [[package]] name = "radroots_storage_sqlite" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "fs2", "radroots_event", @@ -1878,7 +1878,7 @@ dependencies = [ [[package]] name = "radroots_sync" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "radroots_event", "radroots_event_codec", @@ -1893,7 +1893,7 @@ dependencies = [ [[package]] name = "radroots_trade" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "hex", "radroots_core", @@ -1907,7 +1907,7 @@ dependencies = [ [[package]] name = "radroots_transport" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "radroots_event", "radroots_identity", @@ -1919,7 +1919,7 @@ dependencies = [ [[package]] name = "radroots_transport_nostr" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=5667824c6f254a3e7cf8ea0680a38ada9f32fc9e#5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +source = "git+https://github.com/radrootslabs/lib?rev=a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3#a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" dependencies = [ "async-wsocket", "futures", diff --git a/Cargo.toml b/Cargo.toml @@ -23,4 +23,4 @@ todo = "deny" unimplemented = "deny" [workspace.dependencies] -radroots_mobile_ffi = { git = "https://github.com/radrootslabs/lib", rev = "5667824c6f254a3e7cf8ea0680a38ada9f32fc9e", version = "=0.1.0-alpha" } +radroots_mobile_ffi = { git = "https://github.com/radrootslabs/lib", rev = "a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3", version = "=0.1.0-alpha" } diff --git a/Radroots/State/RadrootsAddStore.swift b/Radroots/State/RadrootsAddStore.swift @@ -22,6 +22,7 @@ final class RadrootsAddStore: ObservableObject { @Published private(set) var isCheckingBlossom = false @Published private(set) var isWorking = false @Published private(set) var message: String? + @Published private(set) var lastFailureCode: String? private let runtimeClient: RadrootsRuntimeClient private let media: (any RadrootsAddMediaHandling)? @@ -226,6 +227,14 @@ final class RadrootsAddStore: ObservableObject { status = try await self.uploadPendingMedia(status) } + guard !self.writableRelays.isEmpty else { + self.accept(status) + self.message = status.media.isEmpty + ? "Draft saved. Configure a writable relay to publish." + : "Photo verified and draft saved. Configure a writable relay to publish." + return + } + if status.state.isEditable || status.state == .readyToSign { status = try await self.runtimeClient.queueDraft( id: status.id, @@ -469,6 +478,7 @@ final class RadrootsAddStore: ObservableObject { operationGeneration = requestedGeneration isWorking = true message = nil + lastFailureCode = nil do { try await operation() } catch is CancellationError { @@ -479,6 +489,7 @@ final class RadrootsAddStore: ObservableObject { if requestedGeneration == generation { await refreshBlossomSnapshot() message = Self.message(for: error) + lastFailureCode = Self.failure(for: error)?.code } } if requestedGeneration == generation { @@ -505,15 +516,19 @@ final class RadrootsAddStore: ObservableObject { } private static func message(for error: Error) -> String { - if case let RadrootsRuntimeClientError.add(failure) = error { - return failure.safeMessage - } - if let failure = error as? RadrootsRuntimeFailure { + if let failure = failure(for: error) { return failure.safeMessage } return (error as? LocalizedError)?.errorDescription ?? "The Add operation could not be completed." } + private static func failure(for error: Error) -> RadrootsRuntimeFailure? { + if case let RadrootsRuntimeClientError.add(failure) = error { + return failure + } + return error as? RadrootsRuntimeFailure + } + private static func retractionInput(_ card: RadrootsTodayCard) -> RadrootsRetractionDraftInput { let command = commandType(for: card.type) let kind: UInt32 = switch card.type { diff --git a/Radroots/Views/RadrootsAddView.swift b/Radroots/Views/RadrootsAddView.swift @@ -37,11 +37,6 @@ struct RadrootsAddView: View { .disabled(!store.canSave) .accessibilityIdentifier("radroots.add.save") - Button(submitLabel) { Task { await store.submit() } } - .buttonStyle(.borderedProminent) - .disabled(!store.canSubmit) - .accessibilityIdentifier("radroots.add.submit") - if let active = store.activeDraft, active.state.canCancel { Button("Cancel local work", role: .destructive) { Task { await store.cancel(active) } @@ -53,6 +48,19 @@ struct RadrootsAddView: View { Text("Submit saves an immutable local snapshot first. If signing, media, or a relay is unavailable, the saved operation remains available to retry.") } } + .accessibilityIdentifier("radroots.add.root") + .safeAreaInset(edge: .bottom, spacing: 0) { + Button(submitLabel) { Task { await store.submit() } } + .accessibilityIdentifier("radroots.add.submit") + .accessibilityValue(submitAccessibilityValue) + .buttonStyle(.borderedProminent) + .frame(maxWidth: .infinity) + .padding(.horizontal) + .padding(.vertical, 8) + .padding(.bottom, 72) + .background(.bar) + .disabled(!store.canSubmit) + } .disabled(store.isWorking) .overlay { if store.isWorking { @@ -86,7 +94,6 @@ struct RadrootsAddView: View { RadrootsDraftsSheet(store: store) } .task { await store.start() } - .accessibilityIdentifier("radroots.add.root") } @ViewBuilder @@ -219,6 +226,22 @@ struct RadrootsAddView: View { return types.isEmpty ? RadrootsAddCommandType.allCases : types } + private var submitAccessibilityValue: String { + if store.isWorking { + return "Working" + } + if let message = store.message { + if let code = store.lastFailureCode { + return "\(message) Error code \(code)" + } + return message + } + if let activeDraft = store.activeDraft { + return activeDraft.honestSummary + } + return store.canSubmit ? "Ready" : "Unavailable" + } + private var typeBinding: Binding<RadrootsAddCommandType> { Binding(get: { store.form.commandType }, set: { store.selectType($0) }) } diff --git a/RadrootsFFI/provenance.json b/RadrootsFFI/provenance.json @@ -10,12 +10,12 @@ "files": [], "provenance": { "repository": "https://github.com/radrootslabs/lib", - "revision": "5667824c6f254a3e7cf8ea0680a38ada9f32fc9e", + "revision": "a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3", "architecture": "radroots.crates.release.v2", "catalog_sha256": "8c27cebf6825f9ed74e122513c661f6dd31837dddb39d0988014121d8ab05e75", - "lockfile_sha256": "6e322773bfc212ed354587a00967a81713c82c46c4ad714bcdcb10b66b944244", - "source_archive_sha256": "19b6a81c8f6c41b32d50cefa3744455fcd7b5f1ebeba5e4ce43a815ce4dbe880", - "source_date_epoch": 1786296716, + "lockfile_sha256": "b1e09c89c06cd54190a68db93a8d6f241e85d3dc6b930d3763eea0fbd77d598b", + "source_archive_sha256": "6262b55e4425d6f1b46a884c4794d7989b985d452f9944809699b349d94cd33a", + "source_date_epoch": 1786301798, "builder_id": "radroots_ios_app", "features": [] } diff --git a/RadrootsFFI/source.lock b/RadrootsFFI/source.lock @@ -1,11 +1,11 @@ override RADROOTS_FIELD_LIB_GIT_URL := https://github.com/radrootslabs/lib -override RADROOTS_FIELD_LIB_GIT_REV := 5667824c6f254a3e7cf8ea0680a38ada9f32fc9e +override RADROOTS_FIELD_LIB_GIT_REV := a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3 override RADROOTS_FIELD_FFI_CRATE_VERSION := 0.1.0-alpha -override RADROOTS_FIELD_SOURCE_DATE_EPOCH := 1786296716 -override RADROOTS_FIELD_FFI_DEVICE_SHA256 := 52961d99e9c34786f3fe77ffc11fbf34edeeb0547c41d4b0ac88587a52a0a2bf -override RADROOTS_FIELD_FFI_SIMULATOR_SHA256 := 206930e4776f4c4cd633aa3d9754c3e8a276eaa9fc1a0a735d6939ab1a584637 +override RADROOTS_FIELD_SOURCE_DATE_EPOCH := 1786301798 +override RADROOTS_FIELD_FFI_DEVICE_SHA256 := 68e5ba0d052cdd2112539b735f8a7b8e527fda93778ed18be13196dcedcfa8e3 +override RADROOTS_FIELD_FFI_SIMULATOR_SHA256 := c84b5707a1295a2ffaf5966dba0d90b63cac4bf379e3fdf9d7bb1b918d896130 override RADROOTS_FIELD_FFI_SWIFT_SHA256 := 4e6c9ccc80cbd83a433ac973db1a6649bfec5119600f022192d4e1efeb6b640e override RADROOTS_FIELD_FFI_HEADER_SHA256 := bbbd0db8c09cec9e139ddc8618d160ebe85fa97e776f82e1a007e20684379f40 override RADROOTS_FIELD_FFI_MODULEMAP_SHA256 := b228e67197e2c64b6f5b17a270faf640efc0e298e354b0b3301679e7c4dc321a override RADROOTS_FIELD_FFI_API_SHA256 := 0d4be593220cbeec149eeb14c174e932acbdd00d82e4438ac5d7e88a7d0909ea -override RADROOTS_FIELD_FFI_XCFRAMEWORK_SHA256 := 1508f0b6e915946bb9a3575ccefad81a123aee5f420c45671643979085b8d5f2 +override RADROOTS_FIELD_FFI_XCFRAMEWORK_SHA256 := c94bb6f3095154df9dffbb1c01dc2c82b42b7e6fecaee4dffb28313cee40e824 diff --git a/RadrootsTests/RadrootsAddStoreTests.swift b/RadrootsTests/RadrootsAddStoreTests.swift @@ -144,6 +144,60 @@ final class RadrootsAddStoreTests: XCTestCase { } @MainActor + func testSubmitWithoutWritableRelayVerifiesMediaAndPreservesDraftForRetry() async throws { + let backend = AddBackend(includeWritableRelay: false) + let client = try await Self.startedClient(backend) + let store = RadrootsAddStore( + runtimeClient: client, + media: AddMediaHarness(), + now: { 1_800_000_000 } + ) + await store.configure(snapshot: backend.snapshot()) + await store.start() + store.selectType(.createPhotoUpdate) + store.updateForm(\.content, "Carrots from today") + await store.importPhotos() + + await store.submit() + + XCTAssertEqual(store.activeDraft?.state, .readyToSign) + XCTAssertEqual(store.activeDraft?.media.first?.stage, .verified) + XCTAssertEqual( + store.message, + "Photo verified and draft saved. Configure a writable relay to publish." + ) + XCTAssertNil(store.lastFailureCode) + _ = try await client.stop() + } + + @MainActor + func testSubmitRetainsRedactedFailureCodeForSupportAndAccessibility() async throws { + let failure = RadrootsRuntimeFailure( + schemaVersion: 1, + code: "media_handle_unavailable", + category: "validation", + retryable: false, + recoveryActions: [], + operationID: "add.save", + capabilityID: nil, + safeMessage: "The request is invalid." + ) + let backend = AddBackend(saveFailure: failure) + let client = try await Self.startedClient(backend) + let store = RadrootsAddStore(runtimeClient: client, now: { 1_800_000_000 }) + await store.configure(snapshot: backend.snapshot()) + await store.start() + store.updateForm(\.content, "Prepared media failure") + + await store.submit() + + XCTAssertEqual(store.message, failure.safeMessage) + XCTAssertEqual(store.lastFailureCode, failure.code) + XCTAssertNil(store.activeDraft) + _ = try await client.stop() + } + + @MainActor private func configure(_ store: RadrootsAddStore, type: RadrootsAddCommandType) { switch type { case .createUpdate: @@ -283,13 +337,22 @@ private actor AddMediaHarness: RadrootsAddMediaHandling { private actor AddBackend: RadrootsRuntimeBackend { private let advanceOffline: Bool private let saveDelayNanoseconds: UInt64 + private let saveFailure: RadrootsRuntimeFailure? + private let includeWritableRelay: Bool private var values: [String: RadrootsDraftStatus] = [:] private var uploadAuthorizationCreatedAt: UInt64? private var closed = false - init(advanceOffline: Bool = false, saveDelayNanoseconds: UInt64 = 0) { + init( + advanceOffline: Bool = false, + saveDelayNanoseconds: UInt64 = 0, + saveFailure: RadrootsRuntimeFailure? = nil, + includeWritableRelay: Bool = true + ) { self.advanceOffline = advanceOffline self.saveDelayNanoseconds = saveDelayNanoseconds + self.saveFailure = saveFailure + self.includeWritableRelay = includeWritableRelay } func snapshot() -> RadrootsRuntimeSnapshot { @@ -303,7 +366,7 @@ private actor AddBackend: RadrootsRuntimeBackend { state: "configured", readAvailability: "unobserved", writeAvailability: "unobserved", - relays: [ + relays: includeWritableRelay ? [ RadrootsRelayEndpointStatus( url: "ws://127.0.0.1:7447", access: "read_write", @@ -314,7 +377,7 @@ private actor AddBackend: RadrootsRuntimeBackend { readNextAttemptUnixMilliseconds: nil, writeNextAttemptUnixMilliseconds: nil ), - ] + ] : [] ), blossomConfiguration: RadrootsBlossomConfigurationStatus( schemaVersion: 1, @@ -356,6 +419,9 @@ private actor AddBackend: RadrootsRuntimeBackend { expectedRevision: UInt64?, persistedAtUnixMilliseconds: UInt64 ) async throws -> RadrootsDraftStatus { + if let saveFailure { + throw saveFailure + } if saveDelayNanoseconds > 0 { try await Task.sleep(nanoseconds: saveDelayNanoseconds) } diff --git a/RadrootsUITests/RadrootsRemoteQualificationUITests.swift b/RadrootsUITests/RadrootsRemoteQualificationUITests.swift @@ -74,30 +74,56 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { XCTAssertTrue(library.isEnabled) library.tap() let prepared = app.descendants(matching: .any).matching( - NSPredicate(format: "identifier BEGINSWITH 'radroots.add.media.'") + NSPredicate( + format: "identifier MATCHES %@", + #"radroots\.add\.media\.[0-9a-f]{64}"# + ) ).firstMatch XCTAssertTrue(prepared.waitForExistence(timeout: 30)) let submit = app.descendants(matching: .any)["radroots.add.submit"] - for _ in 0 ..< 5 where !submit.exists { - app.swipeUp() + let addRoot = app.descendants(matching: .any)["radroots.add.root"] + for _ in 0 ..< 8 { + if isUnobscured(submit, above: app.tabBars.firstMatch) { + break + } + addRoot.swipeUp() } guard submit.waitForExistence(timeout: 10), - waitUntilHittable(submit, timeout: 10) + submit.isEnabled, + waitUntilHittable(submit, timeout: 10), + isUnobscured(submit, above: app.tabBars.firstMatch) else { - return XCTFail("The Add submission control did not become hittable") + return XCTFail( + "The Add submission control did not become unobscured; " + + submissionDiagnostics(app, submit: submit) + ) + } + + let priorSubmitValue = submit.value as? String + guard tapSubmitUntilWorkStarts( + app, + submit: submit, + priorSubmitValue: priorSubmitValue + ) else { + return XCTFail( + "The Add submission tap did not start local work; " + + submissionDiagnostics(app, submit: submit) + ) } - let status = app.staticTexts.matching( - identifier: "radroots.add.status" - ).firstMatch - let priorStatusLabel = status.exists ? status.label : nil - submit.tap() guard waitForWorkToFinish( app, submit: submit, - priorStatusLabel: priorStatusLabel + priorSubmitValue: priorSubmitValue ) else { - return XCTFail("The Add submission did not reach a terminal local state") + return XCTFail( + "The Add submission did not reach a terminal local state; " + + submissionDiagnostics(app, submit: submit) + ) + } + let terminalSubmitValue = submit.value as? String ?? "missing" + guard !terminalSubmitValue.contains("Error code") else { + return XCTFail("The Add submission failed locally; submit.value=\(terminalSubmitValue)") } guard openDrafts(app) else { @@ -260,24 +286,77 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { private func waitForWorkToFinish( _ app: XCUIApplication, submit: XCUIElement, - priorStatusLabel: String? + priorSubmitValue: String? ) -> Bool { let progress = app.descendants(matching: .any)["radroots.add.progress"] - let status = app.staticTexts.matching( - identifier: "radroots.add.status" - ).firstMatch let finished = NSPredicate { _, _ in - submit.exists + let submitValue = submit.value as? String + return submit.exists && submit.isEnabled && !progress.exists - && status.exists - && status.label != priorStatusLabel + && submitValue != priorSubmitValue + && submitValue != "Working" } let expectation = XCTNSPredicateExpectation(predicate: finished, object: app) return XCTWaiter.wait(for: [expectation], timeout: 180) == .completed } @MainActor + private func tapSubmitUntilWorkStarts( + _ app: XCUIApplication, + submit: XCUIElement, + priorSubmitValue: String? + ) -> Bool { + let progress = app.descendants(matching: .any)["radroots.add.progress"] + let started = NSPredicate { _, _ in + let submitValue = submit.value as? String + return progress.exists + || submitValue != priorSubmitValue + } + + for _ in 0 ..< 3 { + submit.tap() + let expectation = XCTNSPredicateExpectation(predicate: started, object: app) + if XCTWaiter.wait(for: [expectation], timeout: 5) == .completed { + return true + } + guard waitUntilHittable(submit, timeout: 5), + isUnobscured(submit, above: app.tabBars.firstMatch) + else { + return false + } + } + return false + } + + @MainActor + private func isUnobscured(_ element: XCUIElement, above obstruction: XCUIElement) -> Bool { + guard element.exists, element.isEnabled, element.isHittable, obstruction.exists else { + return false + } + let frame = element.frame + return frame.height > 0 + && frame.minY >= 0 + && frame.maxY <= obstruction.frame.minY + } + + @MainActor + private func submissionDiagnostics( + _ app: XCUIApplication, + submit: XCUIElement + ) -> String { + let progress = app.descendants(matching: .any)["radroots.add.progress"] + let status = app.staticTexts.matching(identifier: "radroots.add.status").firstMatch + let statusLabel = status.exists ? status.label : "missing" + let tabBar = app.tabBars.firstMatch + return "submit.exists=\(submit.exists), submit.enabled=\(submit.isEnabled), " + + "submit.hittable=\(submit.isHittable), submit.value=\(String(describing: submit.value)), " + + "submit.frame=\(submit.frame), " + + "tab_bar_top=\(tabBar.exists ? tabBar.frame.minY : -1), " + + "progress.exists=\(progress.exists), status=\(statusLabel)" + } + + @MainActor private func openDrafts(_ app: XCUIApplication) -> Bool { let drafts = app.buttons["radroots.add.drafts"] guard drafts.waitForExistence(timeout: 10) else { return false } diff --git a/api/RadrootsApp.symbols.json b/api/RadrootsApp.symbols.json @@ -1 +1 @@ -{"module":{"name":"RadrootsApp","platform":{"architecture":"arm64","environment":"simulator","operatingSystem":{"minimumVersion":{"major":18,"minor":0},"name":"ios"},"vendor":"apple"}},"relationships":[{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"c:objc(pl)NSObject"},{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"c:objc(pl)UIApplicationDelegate"},{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"s:SH"},{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"s:SQ"},{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"s:s16SendableMetatypeP"},{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"s:s23CustomStringConvertibleP"},{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"s:s28CustomDebugStringConvertibleP"},{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"s:s7CVarArgP"},{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"s:s8SendableP"},{"kind":"conformsTo","source":"s:11RadrootsApp0aB4ViewV","target":"s:7SwiftUI4ViewP"},{"kind":"conformsTo","source":"s:11RadrootsApp0aB4ViewV","target":"s:s16SendableMetatypeP"},{"kind":"conformsTo","source":"s:11RadrootsApp0aB4ViewV","target":"s:s8SendableP"},{"kind":"conformsTo","source":"s:11RadrootsApp0aB7ReleaseO","target":"s:s16SendableMetatypeP"},{"kind":"conformsTo","source":"s:11RadrootsApp0aB7ReleaseO","target":"s:s8SendableP"},{"kind":"inheritsFrom","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"c:objc(cs)NSObject"},{"kind":"memberOf","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate(im)application:handleEventsForBackgroundURLSession:completionHandler:","target":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate"},{"kind":"memberOf","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate(im)applicationWillTerminate:","target":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate"},{"kind":"memberOf","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate(im)init","target":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate"},{"kind":"memberOf","source":"s:11RadrootsApp0aB4ViewV4bodyQrvp","target":"s:11RadrootsApp0aB4ViewV"},{"kind":"memberOf","source":"s:11RadrootsApp0aB4ViewVACycfc","target":"s:11RadrootsApp0aB4ViewV"},{"kind":"memberOf","source":"s:11RadrootsApp0aB7ReleaseO7versionSSvpZ","target":"s:11RadrootsApp0aB7ReleaseO"},{"kind":"overrides","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate(im)init","target":"c:objc(cs)NSObject(im)init"}],"schema":"radroots.swift-api-snapshot.v1","symbols":[{"access":"public","declaration":[{"kind":"attribute","spelling":"@"},{"kind":"attribute","preciseIdentifier":"s:ScM","spelling":"MainActor"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"final"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"class"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"RadrootsAppDelegate"}],"kind":"swift.class","path":["RadrootsAppDelegate"],"precise":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate"},{"access":"public","declaration":[{"kind":"attribute","spelling":"@"},{"kind":"attribute","preciseIdentifier":"s:ScM","spelling":"MainActor"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"func"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"application"},{"kind":"text","spelling":"("},{"kind":"externalParam","spelling":"_"},{"kind":"text","spelling":": "},{"kind":"typeIdentifier","preciseIdentifier":"c:objc(cs)UIApplication","spelling":"UIApplication"},{"kind":"text","spelling":", "},{"kind":"externalParam","spelling":"handleEventsForBackgroundURLSession"},{"kind":"text","spelling":" "},{"kind":"internalParam","spelling":"identifier"},{"kind":"text","spelling":": "},{"kind":"typeIdentifier","preciseIdentifier":"s:SS","spelling":"String"},{"kind":"text","spelling":", "},{"kind":"externalParam","spelling":"completionHandler"},{"kind":"text","spelling":": "},{"kind":"attribute","spelling":"@escaping "},{"kind":"text","spelling":"() -> "},{"kind":"typeIdentifier","preciseIdentifier":"s:s4Voida","spelling":"Void"},{"kind":"text","spelling":")"}],"kind":"swift.method","path":["RadrootsAppDelegate","application(_:handleEventsForBackgroundURLSession:completionHandler:)"],"precise":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate(im)application:handleEventsForBackgroundURLSession:completionHandler:"},{"access":"public","declaration":[{"kind":"attribute","spelling":"@"},{"kind":"attribute","preciseIdentifier":"s:ScM","spelling":"MainActor"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"func"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"applicationWillTerminate"},{"kind":"text","spelling":"("},{"kind":"externalParam","spelling":"_"},{"kind":"text","spelling":": "},{"kind":"typeIdentifier","preciseIdentifier":"c:objc(cs)UIApplication","spelling":"UIApplication"},{"kind":"text","spelling":")"}],"kind":"swift.method","path":["RadrootsAppDelegate","applicationWillTerminate(_:)"],"precise":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate(im)applicationWillTerminate:"},{"access":"public","declaration":[{"kind":"attribute","spelling":"@"},{"kind":"attribute","preciseIdentifier":"s:ScM","spelling":"MainActor"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"override"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"init"},{"kind":"text","spelling":"()"}],"kind":"swift.init","path":["RadrootsAppDelegate","init()"],"precise":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate(im)init"},{"access":"public","declaration":[{"kind":"attribute","spelling":"@"},{"kind":"attribute","preciseIdentifier":"s:ScM","spelling":"MainActor"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"struct"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"RadrootsAppView"}],"kind":"swift.struct","path":["RadrootsAppView"],"precise":"s:11RadrootsApp0aB4ViewV"},{"access":"public","declaration":[{"kind":"attribute","spelling":"@"},{"kind":"attribute","preciseIdentifier":"s:ScM","spelling":"MainActor"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"var"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"body"},{"kind":"text","spelling":": "},{"kind":"keyword","spelling":"some"},{"kind":"text","spelling":" "},{"kind":"typeIdentifier","preciseIdentifier":"s:7SwiftUI4ViewP","spelling":"View"},{"kind":"text","spelling":" { "},{"kind":"keyword","spelling":"get"},{"kind":"text","spelling":" }"}],"kind":"swift.property","path":["RadrootsAppView","body"],"precise":"s:11RadrootsApp0aB4ViewV4bodyQrvp"},{"access":"public","declaration":[{"kind":"attribute","spelling":"@"},{"kind":"attribute","preciseIdentifier":"s:ScM","spelling":"MainActor"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"init"},{"kind":"text","spelling":"()"}],"kind":"swift.init","path":["RadrootsAppView","init()"],"precise":"s:11RadrootsApp0aB4ViewVACycfc"},{"access":"public","declaration":[{"kind":"keyword","spelling":"enum"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"RadrootsAppRelease"}],"kind":"swift.enum","path":["RadrootsAppRelease"],"precise":"s:11RadrootsApp0aB7ReleaseO"},{"access":"public","declaration":[{"kind":"keyword","spelling":"static"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"let"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"version"},{"kind":"text","spelling":": "},{"kind":"typeIdentifier","preciseIdentifier":"s:SS","spelling":"String"}],"kind":"swift.type.property","path":["RadrootsAppRelease","version"],"precise":"s:11RadrootsApp0aB7ReleaseO7versionSSvpZ"}]} +{"module":{"name":"RadrootsApp","platform":{"architecture":"arm64","environment":"simulator","operatingSystem":{"minimumVersion":{"major":18,"minor":0},"name":"ios"},"vendor":"apple"}},"relationships":[{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"c:objc(pl)NSObject"},{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"c:objc(pl)UIApplicationDelegate"},{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"s:SH"},{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"s:SQ"},{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"s:s16SendableMetatypeP"},{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"s:s23CustomStringConvertibleP"},{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"s:s28CustomDebugStringConvertibleP"},{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"s:s7CVarArgP"},{"kind":"conformsTo","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"s:s8SendableP"},{"kind":"conformsTo","source":"s:11RadrootsApp0aB4ViewV","target":"s:7SwiftUI4ViewP"},{"kind":"conformsTo","source":"s:11RadrootsApp0aB4ViewV","target":"s:s16SendableMetatypeP"},{"kind":"conformsTo","source":"s:11RadrootsApp0aB4ViewV","target":"s:s8SendableP"},{"kind":"conformsTo","source":"s:11RadrootsApp0aB7ReleaseO","target":"s:s16SendableMetatypeP"},{"kind":"conformsTo","source":"s:11RadrootsApp0aB7ReleaseO","target":"s:s8SendableP"},{"kind":"inheritsFrom","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate","target":"c:objc(cs)NSObject"},{"kind":"memberOf","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate(im)application:didFinishLaunchingWithOptions:","target":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate"},{"kind":"memberOf","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate(im)application:handleEventsForBackgroundURLSession:completionHandler:","target":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate"},{"kind":"memberOf","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate(im)applicationWillTerminate:","target":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate"},{"kind":"memberOf","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate(im)init","target":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate"},{"kind":"memberOf","source":"s:11RadrootsApp0aB4ViewV4bodyQrvp","target":"s:11RadrootsApp0aB4ViewV"},{"kind":"memberOf","source":"s:11RadrootsApp0aB4ViewVACycfc","target":"s:11RadrootsApp0aB4ViewV"},{"kind":"memberOf","source":"s:11RadrootsApp0aB7ReleaseO7versionSSvpZ","target":"s:11RadrootsApp0aB7ReleaseO"},{"kind":"overrides","source":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate(im)init","target":"c:objc(cs)NSObject(im)init"}],"schema":"radroots.swift-api-snapshot.v1","symbols":[{"access":"public","declaration":[{"kind":"attribute","spelling":"@"},{"kind":"attribute","preciseIdentifier":"s:ScM","spelling":"MainActor"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"final"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"class"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"RadrootsAppDelegate"}],"kind":"swift.class","path":["RadrootsAppDelegate"],"precise":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate"},{"access":"public","declaration":[{"kind":"attribute","spelling":"@"},{"kind":"attribute","preciseIdentifier":"s:ScM","spelling":"MainActor"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"func"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"application"},{"kind":"text","spelling":"("},{"kind":"externalParam","spelling":"_"},{"kind":"text","spelling":" "},{"kind":"internalParam","spelling":"application"},{"kind":"text","spelling":": "},{"kind":"typeIdentifier","preciseIdentifier":"c:objc(cs)UIApplication","spelling":"UIApplication"},{"kind":"text","spelling":", "},{"kind":"externalParam","spelling":"didFinishLaunchingWithOptions"},{"kind":"text","spelling":" "},{"kind":"internalParam","spelling":"_"},{"kind":"text","spelling":": ["},{"kind":"typeIdentifier","preciseIdentifier":"c:objc(cs)UIApplication","spelling":"UIApplication"},{"kind":"text","spelling":"."},{"kind":"typeIdentifier","preciseIdentifier":"c:@T@UIApplicationLaunchOptionsKey","spelling":"LaunchOptionsKey"},{"kind":"text","spelling":" : "},{"kind":"keyword","spelling":"Any"},{"kind":"text","spelling":"]? = nil) -> "},{"kind":"typeIdentifier","preciseIdentifier":"s:Sb","spelling":"Bool"}],"kind":"swift.method","path":["RadrootsAppDelegate","application(_:didFinishLaunchingWithOptions:)"],"precise":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate(im)application:didFinishLaunchingWithOptions:"},{"access":"public","declaration":[{"kind":"attribute","spelling":"@"},{"kind":"attribute","preciseIdentifier":"s:ScM","spelling":"MainActor"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"func"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"application"},{"kind":"text","spelling":"("},{"kind":"externalParam","spelling":"_"},{"kind":"text","spelling":": "},{"kind":"typeIdentifier","preciseIdentifier":"c:objc(cs)UIApplication","spelling":"UIApplication"},{"kind":"text","spelling":", "},{"kind":"externalParam","spelling":"handleEventsForBackgroundURLSession"},{"kind":"text","spelling":" "},{"kind":"internalParam","spelling":"identifier"},{"kind":"text","spelling":": "},{"kind":"typeIdentifier","preciseIdentifier":"s:SS","spelling":"String"},{"kind":"text","spelling":", "},{"kind":"externalParam","spelling":"completionHandler"},{"kind":"text","spelling":": "},{"kind":"attribute","spelling":"@escaping "},{"kind":"text","spelling":"() -> "},{"kind":"typeIdentifier","preciseIdentifier":"s:s4Voida","spelling":"Void"},{"kind":"text","spelling":")"}],"kind":"swift.method","path":["RadrootsAppDelegate","application(_:handleEventsForBackgroundURLSession:completionHandler:)"],"precise":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate(im)application:handleEventsForBackgroundURLSession:completionHandler:"},{"access":"public","declaration":[{"kind":"attribute","spelling":"@"},{"kind":"attribute","preciseIdentifier":"s:ScM","spelling":"MainActor"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"func"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"applicationWillTerminate"},{"kind":"text","spelling":"("},{"kind":"externalParam","spelling":"_"},{"kind":"text","spelling":": "},{"kind":"typeIdentifier","preciseIdentifier":"c:objc(cs)UIApplication","spelling":"UIApplication"},{"kind":"text","spelling":")"}],"kind":"swift.method","path":["RadrootsAppDelegate","applicationWillTerminate(_:)"],"precise":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate(im)applicationWillTerminate:"},{"access":"public","declaration":[{"kind":"attribute","spelling":"@"},{"kind":"attribute","preciseIdentifier":"s:ScM","spelling":"MainActor"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"override"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"init"},{"kind":"text","spelling":"()"}],"kind":"swift.init","path":["RadrootsAppDelegate","init()"],"precise":"c:@M@RadrootsApp@objc(cs)RadrootsAppDelegate(im)init"},{"access":"public","declaration":[{"kind":"attribute","spelling":"@"},{"kind":"attribute","preciseIdentifier":"s:ScM","spelling":"MainActor"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"struct"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"RadrootsAppView"}],"kind":"swift.struct","path":["RadrootsAppView"],"precise":"s:11RadrootsApp0aB4ViewV"},{"access":"public","declaration":[{"kind":"attribute","spelling":"@"},{"kind":"attribute","preciseIdentifier":"s:ScM","spelling":"MainActor"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"var"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"body"},{"kind":"text","spelling":": "},{"kind":"keyword","spelling":"some"},{"kind":"text","spelling":" "},{"kind":"typeIdentifier","preciseIdentifier":"s:7SwiftUI4ViewP","spelling":"View"},{"kind":"text","spelling":" { "},{"kind":"keyword","spelling":"get"},{"kind":"text","spelling":" }"}],"kind":"swift.property","path":["RadrootsAppView","body"],"precise":"s:11RadrootsApp0aB4ViewV4bodyQrvp"},{"access":"public","declaration":[{"kind":"attribute","spelling":"@"},{"kind":"attribute","preciseIdentifier":"s:ScM","spelling":"MainActor"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"init"},{"kind":"text","spelling":"()"}],"kind":"swift.init","path":["RadrootsAppView","init()"],"precise":"s:11RadrootsApp0aB4ViewVACycfc"},{"access":"public","declaration":[{"kind":"keyword","spelling":"enum"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"RadrootsAppRelease"}],"kind":"swift.enum","path":["RadrootsAppRelease"],"precise":"s:11RadrootsApp0aB7ReleaseO"},{"access":"public","declaration":[{"kind":"keyword","spelling":"static"},{"kind":"text","spelling":" "},{"kind":"keyword","spelling":"let"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"version"},{"kind":"text","spelling":": "},{"kind":"typeIdentifier","preciseIdentifier":"s:SS","spelling":"String"}],"kind":"swift.type.property","path":["RadrootsAppRelease","version"],"precise":"s:11RadrootsApp0aB7ReleaseO7versionSSvpZ"}]} diff --git a/radroots.lib.source-lock.v1.toml b/radroots.lib.source-lock.v1.toml @@ -1,8 +1,8 @@ schema = "radroots.lib.source-lock.v1" repository = "https://github.com/radrootslabs/lib" -revision = "5667824c6f254a3e7cf8ea0680a38ada9f32fc9e" +revision = "a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3" architecture = "radroots.crates.release.v2" workspace_catalog_sha256 = "8c27cebf6825f9ed74e122513c661f6dd31837dddb39d0988014121d8ab05e75" version = "0.1.0-alpha" -source_archive_sha256 = "19b6a81c8f6c41b32d50cefa3744455fcd7b5f1ebeba5e4ce43a815ce4dbe880" -lockfile_sha256 = "6e322773bfc212ed354587a00967a81713c82c46c4ad714bcdcb10b66b944244" +source_archive_sha256 = "6262b55e4425d6f1b46a884c4794d7989b985d452f9944809699b349d94cd33a" +lockfile_sha256 = "b1e09c89c06cd54190a68db93a8d6f241e85d3dc6b930d3763eea0fbd77d598b" diff --git a/scripts/xcode.sh b/scripts/xcode.sh @@ -125,10 +125,15 @@ case "$operation" in destination=${2:?physical UI qualification requires a device destination} test_selector=${3:?physical UI qualification requires a RadrootsUITests selector} result_name=${4:-} + physical_automation=${RADROOTS_IOS_UI_TEST_PHYSICAL_AUTOMATION:-} development_team=${RADROOTS_IOS_DEVELOPMENT_TEAM:?RADROOTS_IOS_DEVELOPMENT_TEAM is required} qualification_run_id=${RADROOTS_IOS_UI_TEST_RUN_ID:?RADROOTS_IOS_UI_TEST_RUN_ID is required} blossom_origins=${RADROOTS_IOS_UI_TEST_BLOSSOM_ORIGINS:?RADROOTS_IOS_UI_TEST_BLOSSOM_ORIGINS is required} relay_urls=${RADROOTS_IOS_UI_TEST_NOSTR_RELAY_URLS:-} + if [[ "$physical_automation" != "1" ]]; then + echo "error: physical UI qualification requires RADROOTS_IOS_UI_TEST_PHYSICAL_AUTOMATION=1" >&2 + exit 64 + fi if [[ ! "$destination" =~ ^id=[A-Fa-f0-9-]+$ ]]; then echo "error: physical-ui-test destination must be one exact device id" >&2 exit 64 @@ -145,6 +150,26 @@ case "$operation" in echo "error: physical-ui-test run id is invalid" >&2 exit 64 fi + device_id=${destination#id=} + lock_state_file=$(mktemp "${TMPDIR:-/tmp}/radroots-ios-lock-state.XXXXXX") + trap 'unlink "$lock_state_file"' EXIT + if ! xcrun devicectl device info lockState \ + --device "$device_id" \ + --quiet \ + --timeout 10 \ + --json-output "$lock_state_file" + then + echo "error: physical UI qualification device lock state is unavailable" >&2 + exit 1 + fi + passcode_required=$(/usr/bin/plutil \ + -extract result.passcodeRequired raw -o - "$lock_state_file") + unlocked_since_boot=$(/usr/bin/plutil \ + -extract result.unlockedSinceBoot raw -o - "$lock_state_file") + if [[ "$passcode_required" != "false" || "$unlocked_since_boot" != "true" ]]; then + echo "error: physical UI qualification device is locked; refusing to invoke Xcode" >&2 + exit 1 + fi physical_args=( -project Radroots.xcodeproj \ -scheme Radroots \