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:
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 \