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 d1c36a8617f5893351408aee3f7fa840761a4615
parent 16e1fbbe669c662ca82ac95de98df2fc6728159f
Author: triesap <tyson@radroots.org>
Date:   Sat, 29 Aug 2026 05:18:02 +0000

ios: qualify local-social development candidate

- add governed Swift formatting, linting, and Linux x86_64 Rust lanes
- make simulator text sizing deterministic and restore ambient state
- harden visible save, media, and persisted-outbox UI evidence
- bind package, source-lock, API, artifact, and runtime qualification

Diffstat:
M.gitignore | 2++
A.swiftformat | 4++++
A.swiftlint.yml | 24++++++++++++++++++++++++
MAGENTS.md | 6++++++
MMakefile | 10+++++++++-
MPackage.swift | 162++++++++++++++++++++++++++++++++++++++++----------------------------------------
MREADME.md | 5+++++
MRadroots/App/AppEntry.swift | 2+-
MRadroots/App/RadrootsAppDelegate.swift | 14+++++++-------
MRadroots/App/RadrootsAppModel.swift | 2+-
MRadroots/App/RadrootsProvider.swift | 4++--
MRadroots/App/RadrootsRemoteQualification.swift | 2+-
MRadroots/Runtime/RadrootsAddMediaCoordinator.swift | 6+++---
MRadroots/Runtime/RadrootsGeneratedRuntimeBackend.swift | 4++--
MRadroots/Runtime/RadrootsLifecycleCoordinator.swift | 3++-
MRadroots/Runtime/RadrootsRuntimeClient.swift | 38+++++++++++++++++++-------------------
MRadroots/Runtime/RadrootsRuntimeModels.swift | 22+++++++++++-----------
MRadroots/State/RadrootsAddStore.swift | 13++++++-------
MRadroots/State/RadrootsConfigurationStore.swift | 290++++++++++++++++++++++++++++++++++++++++----------------------------------------
MRadroots/State/RadrootsIdentityStore.swift | 114++++++++++++++++++++++++++++++++++++++++----------------------------------------
MRadroots/State/RadrootsMediaStore.swift | 2+-
MRadroots/State/RadrootsSessionStore.swift | 4++--
MRadroots/State/RadrootsSupportingStores.swift | 68++++++++++++++++++++++++++++++++++----------------------------------
MRadroots/State/RadrootsTodayStore.swift | 28++++++++++++++--------------
MRadroots/Views/RadrootsAddView.swift | 5+++--
MRadroots/Views/RadrootsSupportingViews.swift | 16++++++++--------
MRadroots/Views/RadrootsTodayView.swift | 10+++++-----
MRadroots/Views/RuntimeStatusView.swift | 6+++---
MRadrootsTests/RadrootsAddStoreTests.swift | 44+++++++++++++++++++++++++-------------------
MRadrootsTests/RadrootsLifecycleTests.swift | 12++++++------
MRadrootsTests/RadrootsMediaStoreTests.swift | 162++++++++++++++++++++++++++++++++++++++++----------------------------------------
MRadrootsTests/RadrootsRemoteQualificationTests.swift | 15+++++++--------
MRadrootsTests/RadrootsRootShellTests.swift | 68++++++++++++++++++++++++++++++++++----------------------------------
MRadrootsTests/RadrootsRuntimeClientTests.swift | 188++++++++++++++++++++++++++++++++++++++++----------------------------------------
MRadrootsTests/RadrootsStateMigrationTests.swift | 486++++++++++++++++++++++++++++++++++++++++----------------------------------------
MRadrootsTests/RadrootsSupportingStoreTests.swift | 352++++++++++++++++++++++++++++++++++++++++----------------------------------------
MRadrootsTests/RadrootsTodayStoreTests.swift | 358++++++++++++++++++++++++++++++++++++++++----------------------------------------
MRadrootsUITests/RadrootsRemoteQualificationUITests.swift | 187+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
MRadrootsUITests/RadrootsRootShellUITests.swift | 6+++---
Ascripts/linux-shared-rust.sh | 36++++++++++++++++++++++++++++++++++++
Ascripts/swift-quality.sh | 31+++++++++++++++++++++++++++++++
Mscripts/verify-package-contract.sh | 15+++++++++++++++
Mscripts/xcode.sh | 29+++++++++++++++--------------
43 files changed, 1544 insertions(+), 1311 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -49,6 +49,8 @@ Thumbs.db # Testing test*.json +__pycache__/ +*.py[cod] # Editors .vscode/ diff --git a/.swiftformat b/.swiftformat @@ -0,0 +1,4 @@ +--exclude .build,RadrootsFFI/.build,Radroots/Generated,Radroots.xcodeproj,.swiftpm +--indent 2 +--swift-version 6.0 +--disable extensionAccessControl,indent,redundantFileprivate diff --git a/.swiftlint.yml b/.swiftlint.yml @@ -0,0 +1,24 @@ +excluded: + - .build + - .swiftpm + - Radroots.xcodeproj + - Radroots/Generated + - RadrootsFFI/.build + +disabled_rules: + # SwiftFormat is authoritative for overlapping structural style rules. + - closure_parameter_position + - opening_brace + - trailing_comma + # Existing package architecture is governed by tests and API checks; these + # metric and naming policies are not retrofit as Step 070 admission gates. + - cyclomatic_complexity + - file_length + - function_body_length + - function_parameter_count + - identifier_name + - large_tuple + - line_length + - multiple_closures_with_trailing_closure + - type_body_length + - type_name diff --git a/AGENTS.md b/AGENTS.md @@ -76,6 +76,12 @@ This file applies to the complete standalone iOS app repository. A closer `cargo extbuild run -- ...`. - `make package-contract-check` is the narrow standalone source-lock, package-lock, privacy, version, and forbidden-root guard. +- `make swift-quality` applies the checked-in SwiftFormat and SwiftLint policy + to repository-owned package, app, unit-test, API-test, and UI-test sources; + generated bindings and dependency/build output are excluded. +- `make linux-shared-rust` runs the locked source-lock workspace in the pinned + Linux x86_64 Rust runner while keeping Cargo caches and output under the + extbuild project root. - `make bootstrap` performs networked source/artifact bootstrap. `make verify` is the complete package, Xcode build/test, UI, and API-snapshot lane. Use an explicitly installed simulator name when the default is unavailable. diff --git a/Makefile b/Makefile @@ -8,6 +8,8 @@ SIMULATOR_DESTINATION := platform=iOS Simulator,name=$(SIMULATOR_NAME) .NOTPARALLEL: .PHONY: all doctor bootstrap ffi-bootstrap artifact-check package-contract-check \ + swift-quality \ + linux-shared-rust \ package-resolve package-build package-test project xcodegen xcode-resolve \ xcode-build-debug xcode-build-release unit-test ui-test api-snapshot-write \ api-snapshot-check release-evidence-write release-preflight verify clean distclean @@ -26,6 +28,12 @@ artifact-check: doctor package-contract-check: doctor cargo extbuild run -- scripts/verify-package-contract.sh +swift-quality: doctor + cargo extbuild run -- scripts/swift-quality.sh + +linux-shared-rust: doctor + cargo extbuild run -- scripts/linux-shared-rust.sh + package-resolve: artifact-check package-contract-check cargo extbuild run -- scripts/swift-package.sh resolve @@ -67,7 +75,7 @@ release-evidence-write: doctor release-preflight: artifact-check package-contract-check cargo extbuild run -- scripts/release-preflight.sh -verify: artifact-check package-contract-check package-build package-test \ +verify: swift-quality linux-shared-rust artifact-check package-contract-check package-build package-test \ xcode-build-debug xcode-build-release unit-test ui-test api-snapshot-check clean: doctor diff --git a/Package.swift b/Package.swift @@ -3,85 +3,85 @@ import PackageDescription let package = Package( - name: "radroots_ios_app", - platforms: [ - .iOS(.v18), - ], - products: [ - .library(name: "RadrootsApp", targets: ["RadrootsApp"]), - ], - dependencies: [ - .package( - url: "https://github.com/radrootslabs/apple_kit.git", - revision: "e61386529422a6413a852f0233a196a88f4ded51" - ), - ], - targets: [ - .binaryTarget( - name: "RadrootsFFI", - path: "Radroots/Frameworks/RadrootsFFI.xcframework" - ), - .target( - name: "RadrootsKitBindings", - dependencies: ["RadrootsFFI"], - path: "Radroots/Generated", - swiftSettings: [ - .swiftLanguageMode(.v5), - ] - ), - .target( - name: "RadrootsApp", - dependencies: [ - "RadrootsKitBindings", - .product(name: "RadrootsKit", package: "apple_kit"), - ], - path: "Radroots", - exclude: [ - "App/App.swift", - "Config", - "Frameworks", - "Generated", - "Info.plist", - "radroots.xcconfig", - ], - sources: [ - "App/AppEntry.swift", - "App/RadrootsAppDelegate.swift", - "App/RadrootsAppModel.swift", - "App/RadrootsProvider.swift", - "App/RadrootsRemoteQualification.swift", - "App/RadrootsRootShell.swift", - "Runtime/RadrootsAddMediaCoordinator.swift", - "Runtime/RadrootsGeneratedRuntimeBackend.swift", - "Runtime/RadrootsLifecycleCoordinator.swift", - "Runtime/RadrootsRuntimeClient.swift", - "Runtime/RadrootsRuntimeModels.swift", - "State/RadrootsAddStore.swift", - "State/RadrootsConfigurationStore.swift", - "State/RadrootsIdentityStore.swift", - "State/RadrootsMediaStore.swift", - "State/RadrootsSessionStore.swift", - "State/RadrootsSupportingStores.swift", - "State/RadrootsTodayStore.swift", - "Views/RadrootsAddView.swift", - "Views/RadrootsSupportingViews.swift", - "Views/RadrootsTodayView.swift", - "Views/RuntimeStatusView.swift", - ], - resources: [ - .process("Resources/PrivacyInfo.xcprivacy"), - ] - ), - .testTarget( - name: "RadrootsAppTests", - dependencies: ["RadrootsApp"], - path: "RadrootsTests" - ), - .testTarget( - name: "RadrootsAppPublicAPITests", - dependencies: ["RadrootsApp"], - path: "RadrootsPublicAPITests" - ), - ], - swiftLanguageModes: [.v6] + name: "radroots_ios_app", + platforms: [ + .iOS(.v18), + ], + products: [ + .library(name: "RadrootsApp", targets: ["RadrootsApp"]), + ], + dependencies: [ + .package( + url: "https://github.com/radrootslabs/apple_kit.git", + revision: "e61386529422a6413a852f0233a196a88f4ded51" + ), + ], + targets: [ + .binaryTarget( + name: "RadrootsFFI", + path: "Radroots/Frameworks/RadrootsFFI.xcframework" + ), + .target( + name: "RadrootsKitBindings", + dependencies: ["RadrootsFFI"], + path: "Radroots/Generated", + swiftSettings: [ + .swiftLanguageMode(.v5), + ] + ), + .target( + name: "RadrootsApp", + dependencies: [ + "RadrootsKitBindings", + .product(name: "RadrootsKit", package: "apple_kit"), + ], + path: "Radroots", + exclude: [ + "App/App.swift", + "Config", + "Frameworks", + "Generated", + "Info.plist", + "radroots.xcconfig", + ], + sources: [ + "App/AppEntry.swift", + "App/RadrootsAppDelegate.swift", + "App/RadrootsAppModel.swift", + "App/RadrootsProvider.swift", + "App/RadrootsRemoteQualification.swift", + "App/RadrootsRootShell.swift", + "Runtime/RadrootsAddMediaCoordinator.swift", + "Runtime/RadrootsGeneratedRuntimeBackend.swift", + "Runtime/RadrootsLifecycleCoordinator.swift", + "Runtime/RadrootsRuntimeClient.swift", + "Runtime/RadrootsRuntimeModels.swift", + "State/RadrootsAddStore.swift", + "State/RadrootsConfigurationStore.swift", + "State/RadrootsIdentityStore.swift", + "State/RadrootsMediaStore.swift", + "State/RadrootsSessionStore.swift", + "State/RadrootsSupportingStores.swift", + "State/RadrootsTodayStore.swift", + "Views/RadrootsAddView.swift", + "Views/RadrootsSupportingViews.swift", + "Views/RadrootsTodayView.swift", + "Views/RuntimeStatusView.swift", + ], + resources: [ + .process("Resources/PrivacyInfo.xcprivacy"), + ] + ), + .testTarget( + name: "RadrootsAppTests", + dependencies: ["RadrootsApp"], + path: "RadrootsTests" + ), + .testTarget( + name: "RadrootsAppPublicAPITests", + dependencies: ["RadrootsApp"], + path: "RadrootsPublicAPITests" + ), + ], + swiftLanguageModes: [.v6] ) diff --git a/README.md b/README.md @@ -50,6 +50,11 @@ resolved revisions without automatic dependency updates: make verify ``` +The complete lane includes the repository-owned Swift formatting and lint +policy and the pinned Linux x86_64 shared-Rust runner. Run those focused checks +independently with `make swift-quality` and `make linux-shared-rust`; both keep +their build output under extbuild. + Use `SIMULATOR_NAME="Device Name" make verify` when the default simulator is not installed. `make clean` removes only rebuildable external build output and the ignored XCFramework; it preserves tracked generated bindings, locks, diff --git a/Radroots/App/AppEntry.swift b/Radroots/App/AppEntry.swift @@ -19,7 +19,7 @@ struct AppEntry: View { var body: some View { Group { - if case .running(let snapshot) = appModel.phase { + if case let .running(snapshot) = appModel.phase { RadrootsRootShell( snapshot: snapshot, stores: appModel.productStores diff --git a/Radroots/App/RadrootsAppDelegate.swift b/Radroots/App/RadrootsAppDelegate.swift @@ -6,8 +6,8 @@ public final class RadrootsAppDelegate: NSObject, UIApplicationDelegate { } public func application( - _ application: UIApplication, - didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil + _ application: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { #if DEBUG if (try? RadrootsRemoteQualificationEnvironment.current()) != nil { @@ -18,15 +18,15 @@ public final class RadrootsAppDelegate: NSObject, UIApplicationDelegate { } public func application( - _: UIApplication, - handleEventsForBackgroundURLSession identifier: String, - completionHandler: @escaping () -> Void + _: UIApplication, + handleEventsForBackgroundURLSession identifier: String, + completionHandler: @escaping () -> Void ) { let completion = RadrootsCompletionOnce(completionHandler) Task { await RadrootsBackgroundEventRouter.shared.handle( - identifier: identifier, - completion: completion + identifier: identifier, + completion: completion ) } } diff --git a/Radroots/App/RadrootsAppModel.swift b/Radroots/App/RadrootsAppModel.swift @@ -271,7 +271,7 @@ final class RadrootsAppModel: ObservableObject { await finishSessionOperation() return } - if case .running(let snapshot) = result { + if case let .running(snapshot) = result { productStores?.configure(snapshot: snapshot) } phase = result diff --git a/Radroots/App/RadrootsProvider.swift b/Radroots/App/RadrootsProvider.swift @@ -8,8 +8,8 @@ struct RadrootsProvider<Content: View>: View { private let content: () -> Content init( - appModel: @autoclosure @escaping () -> RadrootsAppModel = RadrootsAppModel(), - @ViewBuilder content: @escaping () -> Content + appModel: @autoclosure @escaping () -> RadrootsAppModel = RadrootsAppModel(), + @ViewBuilder content: @escaping () -> Content ) { _appModel = StateObject(wrappedValue: appModel()) self.content = content diff --git a/Radroots/App/RadrootsRemoteQualification.swift b/Radroots/App/RadrootsRemoteQualification.swift @@ -110,7 +110,7 @@ struct RadrootsRemoteQualificationEnvironment: Sendable, Equatable { private static func requiredRunID(_ raw: String?) throws -> String { guard let raw, - (8...64).contains(raw.utf8.count), + (8 ... 64).contains(raw.utf8.count), raw == raw.lowercased(), raw.unicodeScalars.allSatisfy({ CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyz0123456789-") diff --git a/Radroots/Runtime/RadrootsAddMediaCoordinator.swift b/Radroots/Runtime/RadrootsAddMediaCoordinator.swift @@ -353,7 +353,7 @@ actor RadrootsAddMediaCoordinator: RadrootsAddMediaHandling { return snapshot } let candidates = parsed.filter { snapshot in - return snapshot.state != .completed + snapshot.state != .completed || Self.persistedRequestMatches(snapshot.request, request: request) } try Task.checkCancellation() @@ -544,7 +544,7 @@ actor RadrootsAddMediaCoordinator: RadrootsAddMediaHandling { ) } try RadrootsAppleFileAccess(roots: roots).write( - .inline(try RadrootsRemoteQualificationEnvironment.mediaFixtureData()), + .inline(RadrootsRemoteQualificationEnvironment.mediaFixtureData()), to: file ) let url = try roots.resolvedURL(for: file) @@ -554,7 +554,7 @@ actor RadrootsAddMediaCoordinator: RadrootsAddMediaHandling { guard values.isRegularFile == true, values.isSymbolicLink != true, let size = values.fileSize, - (1...40 * 1024 * 1024).contains(size) + (1 ... 40 * 1024 * 1024).contains(size) else { throw RadrootsCaptureIntakeError.unavailable( "remote qualification image is unavailable" diff --git a/Radroots/Runtime/RadrootsGeneratedRuntimeBackend.swift b/Radroots/Runtime/RadrootsGeneratedRuntimeBackend.swift @@ -638,7 +638,7 @@ private final class RadrootsGeneratedRuntimeBackend: RadrootsRuntimeBackend, @un } private static func failure(from error: Error) -> RadrootsRuntimeFailure { - if case RadrootsAppError.Failure(let report) = error { + if case let RadrootsAppError.Failure(report) = error { return RadrootsRuntimeFailure( schemaVersion: report.schemaVersion, code: report.code, @@ -781,7 +781,7 @@ extension RadrootsRuntimeSigningOutcome { } fileprivate var signatureHex: String? { - if case .signed(let signatureHex) = self { + if case let .signed(signatureHex) = self { return signatureHex } return nil diff --git a/Radroots/Runtime/RadrootsLifecycleCoordinator.swift b/Radroots/Runtime/RadrootsLifecycleCoordinator.swift @@ -163,7 +163,8 @@ actor RadrootsDiagnosticsBuffer: RadrootsTelemetry { fields: Dictionary( uniqueKeysWithValues: event.fields.map { field in (field.key, field.value.renderedValue) - }), + } + ), occurredAtUnixMilliseconds: Int64(event.occurredAt.timeIntervalSince1970 * 1000) ) } diff --git a/Radroots/Runtime/RadrootsRuntimeClient.swift b/Radroots/Runtime/RadrootsRuntimeClient.swift @@ -547,7 +547,7 @@ actor RadrootsRuntimeClient { } }, onAbandonedResult: { result in - guard case .success(let started) = result else { return } + guard case let .success(started) = result else { return } let cleanup = RadrootsRuntimeBoundedTask<RadrootsRuntimeShutdownReceipt>( deadlineNanoseconds: cleanupDeadline, operation: { @@ -923,7 +923,7 @@ actor RadrootsRuntimeClient { } }, onAbandonedResult: { result in - guard case .success(let token) = result else { return } + guard case let .success(token) = result else { return } let cancellation = RadrootsRuntimeBoundedTask<Void>( deadlineNanoseconds: subscriptionDeadline, operation: { @@ -946,18 +946,18 @@ actor RadrootsRuntimeClient { var subscription = subscriptions[id] else { subscriptions.removeValue(forKey: id)?.continuation.finish() - if case .completed(.success(let token)) = outcome { + if case let .completed(.success(token)) = outcome { cancelTokenDetached(token) } throw RadrootsRuntimeClientError.superseded } switch outcome { - case .completed(.success(let token)): + case let .completed(.success(token)): subscription.token = token subscriptions[id] = subscription return pair.stream - case .completed(.failure(let failure)): + case let .completed(.failure(failure)): subscriptions.removeValue(forKey: id)?.continuation.finish() throw RadrootsRuntimeClientError.subscription(failure) case .timedOut: @@ -1029,7 +1029,7 @@ actor RadrootsRuntimeClient { if startupOperation?.identity == operation.identity { startupOperation = nil - } else if case .completed(.success(let started)) = outcome, + } else if case let .completed(.success(started)) = outcome, configuration == operation.configuration, case .running = lifecycleState { @@ -1039,12 +1039,12 @@ actor RadrootsRuntimeClient { } switch outcome { - case .completed(.success(let started)): + case let .completed(.success(started)): backend = started.backend configuration = operation.configuration lifecycleState = .running(generation: operation.identity.generation) return started.snapshot - case .completed(.failure(let failure)): + case let .completed(.failure(failure)): lifecycleState = .failed(generation: operation.identity.generation, failure: failure) throw RadrootsRuntimeClientError.startup(failure) case .timedOut: @@ -1140,11 +1140,11 @@ actor RadrootsRuntimeClient { } switch outcome { - case .completed(.success(let receipt)): + case let .completed(.success(receipt)): quarantinedBackend = nil lifecycleState = .stopped return receipt - case .completed(.failure(let failure)): + case let .completed(.failure(failure)): quarantinedBackend = operation.backend lifecycleState = .failed(generation: operation.identity.generation, failure: failure) throw RadrootsRuntimeClientError.shutdown(failure) @@ -1197,9 +1197,9 @@ actor RadrootsRuntimeClient { throw RadrootsRuntimeClientError.superseded } switch outcome { - case .completed(.success(let value)): + case let .completed(.success(value)): return value - case .completed(.failure(let failure)): + case let .completed(.failure(failure)): throw failure case .timedOut: throw Self.deadlineFailure(identity: identity) @@ -1341,25 +1341,25 @@ actor RadrootsRuntimeClient { safeMessage: "The Radroots runtime operation was cancelled." ) } - if case RadrootsRuntimeClientError.startup(let failure) = error { + if case let RadrootsRuntimeClientError.startup(failure) = error { return failure } - if case RadrootsRuntimeClientError.subscription(let failure) = error { + if case let RadrootsRuntimeClientError.subscription(failure) = error { return failure } - if case RadrootsRuntimeClientError.status(let failure) = error { + if case let RadrootsRuntimeClientError.status(failure) = error { return failure } - if case RadrootsRuntimeClientError.today(let failure) = error { + if case let RadrootsRuntimeClientError.today(failure) = error { return failure } - if case RadrootsRuntimeClientError.add(let failure) = error { + if case let RadrootsRuntimeClientError.add(failure) = error { return failure } - if case RadrootsRuntimeClientError.support(let failure) = error { + if case let RadrootsRuntimeClientError.support(failure) = error { return failure } - if case RadrootsRuntimeClientError.shutdown(let failure) = error { + if case let RadrootsRuntimeClientError.shutdown(failure) = error { return failure } return .local( diff --git a/Radroots/Runtime/RadrootsRuntimeModels.swift b/Radroots/Runtime/RadrootsRuntimeModels.swift @@ -467,13 +467,13 @@ extension RadrootsRuntimeClientError: LocalizedError { "The Radroots runtime is not running." case .superseded: "A newer runtime lifecycle request replaced this request." - case .startup(let failure), - .subscription(let failure), - .status(let failure), - .today(let failure), - .add(let failure), - .support(let failure), - .shutdown(let failure): + case let .startup(failure), + let .subscription(failure), + let .status(failure), + let .today(failure), + let .add(failure), + let .support(failure), + let .shutdown(failure): failure.safeMessage } } @@ -717,11 +717,11 @@ struct RadrootsTodayCard: Sendable, Equatable, Hashable, Identifiable { guard sourceAddress == nil else { return nil } return 1 case .event: - guard let kind = sourceAddressKind, kind == 31_922 || kind == 31_923 else { return nil } + guard let kind = sourceAddressKind, kind == 31922 || kind == 31923 else { return nil } return kind case .foodAvailability: - guard sourceAddressKind == 30_402 else { return nil } - return 30_402 + guard sourceAddressKind == 30402 else { return nil } + return 30402 } } @@ -887,7 +887,7 @@ enum RadrootsProductSurfaceContract { guard schemas.map(\.commandType) == expectedCommands, schemas.allSatisfy({ $0.schemaVersion == 1 && $0.label == $0.commandType.label }), schemas.allSatisfy({ schema in - schema.fields.allSatisfy({ $0.schemaVersion == 1 }) + schema.fields.allSatisfy { $0.schemaVersion == 1 } && Set(schema.fields.map(\.id)).count == schema.fields.count }), schemas.allSatisfy({ $0.fields.map(\.id) == expectedFieldIDs(for: $0.commandType) }), diff --git a/Radroots/State/RadrootsAddStore.swift b/Radroots/State/RadrootsAddStore.swift @@ -50,7 +50,7 @@ final class RadrootsAddStore: ObservableObject { UInt64(Date().timeIntervalSince1970) }, nowUnixMilliseconds: @escaping @Sendable () -> UInt64 = { - UInt64(Date().timeIntervalSince1970 * 1_000) + UInt64(Date().timeIntervalSince1970 * 1000) }, observationDelay: @escaping @Sendable (UInt32) async throws -> Void = RadrootsRuntimeObservationBackoff.sleep @@ -514,7 +514,7 @@ final class RadrootsAddStore: ObservableObject { ) try Task.checkCancellation() revisionOperationID = revision.operationID - self.revisionTarget = nil + revisionTarget = nil accept(revision) return revision.replacement } @@ -718,7 +718,7 @@ final class RadrootsAddStore: ObservableObject { lastFailureCode = nil let task = Task { @MainActor [weak self] in guard let self else { return } - await self.execute(operation, generation: requestedGeneration) + await execute(operation, generation: requestedGeneration) } operationTask = task await task.value @@ -771,8 +771,8 @@ final class RadrootsAddStore: ObservableObject { } if type == .createEvent { let now = nowUnixSeconds() - let start = now.addingReportingOverflow(3_600).overflow ? now : now + 3_600 - let end = start.addingReportingOverflow(3_600).overflow ? start : start + 3_600 + let start = now.addingReportingOverflow(3600).overflow ? now : now + 3600 + let end = start.addingReportingOverflow(3600).overflow ? start : start + 3600 form.eventStartUnixSeconds = start form.eventEndUnixSeconds = end form.eventStartDate = eventDateFormatter.string( @@ -810,10 +810,9 @@ final class RadrootsAddStore: ObservableObject { } private static func failure(for error: Error) -> RadrootsRuntimeFailure? { - if case RadrootsRuntimeClientError.add(let failure) = error { + if case let RadrootsRuntimeClientError.add(failure) = error { return failure } return error as? RadrootsRuntimeFailure } - } diff --git a/Radroots/State/RadrootsConfigurationStore.swift b/Radroots/State/RadrootsConfigurationStore.swift @@ -1,5 +1,5 @@ -import Darwin import CryptoKit +import Darwin import Foundation import RadrootsKit import Security @@ -113,20 +113,20 @@ actor RadrootsConfigurationStore { } private static let configurationFile = RadrootsFileReference( - scope: .data, - relativePath: "settings/radroots_configuration_v3.json" + scope: .data, + relativePath: "settings/radroots_configuration_v3.json" ) private static let v2ConfigurationFile = RadrootsFileReference( - scope: .data, - relativePath: "settings/radroots_configuration_v2.json" + scope: .data, + relativePath: "settings/radroots_configuration_v2.json" ) private static let legacyRelayFile = RadrootsFileReference( - scope: .data, - relativePath: "settings/relay_settings.json" + scope: .data, + relativePath: "settings/relay_settings.json" ) private static let sourceGenerationFile = RadrootsFileReference( - scope: .data, - relativePath: "state/source_generation_v1.json" + scope: .data, + relativePath: "state/source_generation_v1.json" ) private let bootstrap: RadrootsConfigurationBootstrap @@ -146,8 +146,8 @@ actor RadrootsConfigurationStore { throw RadrootsConfigurationError.corruptStoredConfiguration } let upgraded = try Self.upgradeStoredConfiguration( - stored, - bootstrapFingerprint: bootstrapFingerprint + stored, + bootstrapFingerprint: bootstrapFingerprint ) if upgraded.profile == profile, Self.blossomMatchesProfile(upgraded.blossom, profile: profile), @@ -159,12 +159,12 @@ actor RadrootsConfigurationStore { } } else { selected = Self.bootstrapConfiguration( - bootstrap, - profile: profile, - generation: (upgraded.generation ?? 0) + 1, - activationState: .reconfigurationRequired, - bootstrapFingerprint: bootstrapFingerprint, - previousBlossomConfigFingerprint: upgraded.canonicalBlossomConfigFingerprint + bootstrap, + profile: profile, + generation: (upgraded.generation ?? 0) + 1, + activationState: .reconfigurationRequired, + bootstrapFingerprint: bootstrapFingerprint, + previousBlossomConfigFingerprint: upgraded.canonicalBlossomConfigFingerprint ?? upgraded.previousBlossomConfigFingerprint ) try persist(selected) @@ -175,26 +175,26 @@ actor RadrootsConfigurationStore { } selected = if stored.profile == profile { StoredConfigurationV3( - format: StoredConfigurationV3.format, - profile: profile, - writableRelays: stored.writableRelays, - blossom: Self.blossomConfiguration( - origins: stored.blossomOrigins, - profile: profile - ), - generation: 1, - activationState: .current, - bootstrapFingerprint: bootstrapFingerprint, - canonicalBlossomConfigFingerprint: nil, - previousBlossomConfigFingerprint: nil + format: StoredConfigurationV3.format, + profile: profile, + writableRelays: stored.writableRelays, + blossom: Self.blossomConfiguration( + origins: stored.blossomOrigins, + profile: profile + ), + generation: 1, + activationState: .current, + bootstrapFingerprint: bootstrapFingerprint, + canonicalBlossomConfigFingerprint: nil, + previousBlossomConfigFingerprint: nil ) } else { Self.bootstrapConfiguration( - bootstrap, - profile: profile, - generation: 1, - activationState: .reconfigurationRequired, - bootstrapFingerprint: bootstrapFingerprint + bootstrap, + profile: profile, + generation: 1, + activationState: .reconfigurationRequired, + bootstrapFingerprint: bootstrapFingerprint ) } try persist(selected) @@ -203,34 +203,34 @@ actor RadrootsConfigurationStore { throw RadrootsConfigurationError.corruptStoredConfiguration } selected = StoredConfigurationV3( - format: StoredConfigurationV3.format, - profile: profile, - writableRelays: legacy.relays, - blossom: Self.blossomConfiguration( - origins: bootstrap.blossomOrigins, - profile: profile - ), - generation: 1, - activationState: .current, - bootstrapFingerprint: bootstrapFingerprint, - canonicalBlossomConfigFingerprint: nil, - previousBlossomConfigFingerprint: nil + format: StoredConfigurationV3.format, + profile: profile, + writableRelays: legacy.relays, + blossom: Self.blossomConfiguration( + origins: bootstrap.blossomOrigins, + profile: profile + ), + generation: 1, + activationState: .current, + bootstrapFingerprint: bootstrapFingerprint, + canonicalBlossomConfigFingerprint: nil, + previousBlossomConfigFingerprint: nil ) try persist(selected) } else { selected = Self.bootstrapConfiguration( - bootstrap, - profile: profile, - generation: 1, - activationState: .current, - bootstrapFingerprint: bootstrapFingerprint + bootstrap, + profile: profile, + generation: 1, + activationState: .current, + bootstrapFingerprint: bootstrapFingerprint ) try persist(selected) } let relays = try RadrootsNetworkValidator.relays( - selected.writableRelays, - profile: profile + selected.writableRelays, + profile: profile ) guard !bootstrap.keychainServicePrefix.isEmpty, !bootstrap.bundleIdentifier.isEmpty @@ -238,21 +238,21 @@ actor RadrootsConfigurationStore { throw RadrootsConfigurationError.missing("identity") } return RadrootsAppConfiguration( - profile: profile, - writableRelays: relays, - blossom: selected.blossom, - keychainServicePrefix: bootstrap.keychainServicePrefix, - bundleIdentifier: bootstrap.bundleIdentifier, - appMetadata: bootstrap.appMetadata, - generation: selected.generation ?? 1, - activationState: selected.activationState ?? .current, - previousBlossomConfigFingerprint: selected.previousBlossomConfigFingerprint + profile: profile, + writableRelays: relays, + blossom: selected.blossom, + keychainServicePrefix: bootstrap.keychainServicePrefix, + bundleIdentifier: bootstrap.bundleIdentifier, + appMetadata: bootstrap.appMetadata, + generation: selected.generation ?? 1, + activationState: selected.activationState ?? .current, + previousBlossomConfigFingerprint: selected.previousBlossomConfigFingerprint ) } func confirmCanonicalBlossomConfiguration( - _ canonical: RadrootsBlossomConfigurationStatus?, - expectedGeneration: UInt64 + _ canonical: RadrootsBlossomConfigurationStatus?, + expectedGeneration: UInt64 ) throws { guard let stored = try readStoredConfiguration(), stored.format == StoredConfigurationV3.format, @@ -279,10 +279,10 @@ actor RadrootsConfigurationStore { throw RadrootsConfigurationError.invalid("canonical_blossom_configuration") } canonicalBlossom = RadrootsBlossomEndpointConfiguration( - hostKind: hostKind, - endpointAuthority: endpointAuthority, - primaryOrigin: canonical.primaryOrigin, - fallbackOrigins: canonical.fallbackOrigins + hostKind: hostKind, + endpointAuthority: endpointAuthority, + primaryOrigin: canonical.primaryOrigin, + fallbackOrigins: canonical.fallbackOrigins ) canonicalFingerprint = canonical.configFingerprint default: @@ -290,15 +290,15 @@ actor RadrootsConfigurationStore { } try persist( StoredConfigurationV3( - format: StoredConfigurationV3.format, - profile: stored.profile, - writableRelays: stored.writableRelays, - blossom: canonicalBlossom, - generation: expectedGeneration, - activationState: .current, - bootstrapFingerprint: bootstrapFingerprint, - canonicalBlossomConfigFingerprint: canonicalFingerprint, - previousBlossomConfigFingerprint: nil + format: StoredConfigurationV3.format, + profile: stored.profile, + writableRelays: stored.writableRelays, + blossom: canonicalBlossom, + generation: expectedGeneration, + activationState: .current, + bootstrapFingerprint: bootstrapFingerprint, + canonicalBlossomConfigFingerprint: canonicalFingerprint, + previousBlossomConfigFingerprint: nil ) ) } @@ -313,15 +313,15 @@ actor RadrootsConfigurationStore { } try persist( StoredConfigurationV3( - format: stored.format, - profile: stored.profile, - writableRelays: stored.writableRelays, - blossom: stored.blossom, - generation: expectedGeneration, - activationState: .current, - bootstrapFingerprint: bootstrapFingerprint, - canonicalBlossomConfigFingerprint: nil, - previousBlossomConfigFingerprint: nil + format: stored.format, + profile: stored.profile, + writableRelays: stored.writableRelays, + blossom: stored.blossom, + generation: expectedGeneration, + activationState: .current, + bootstrapFingerprint: bootstrapFingerprint, + canonicalBlossomConfigFingerprint: nil, + previousBlossomConfigFingerprint: nil ) ) } @@ -344,9 +344,9 @@ actor RadrootsConfigurationStore { throw RadrootsConfigurationError.persistenceFailed } let value = RadrootsSourceGeneration( - schemaVersion: 1, - generationHex: bytes.map { String(format: "%02x", $0) }.joined(), - createdAtUnixMilliseconds: max(1, UInt64(Date().timeIntervalSince1970 * 1000)) + schemaVersion: 1, + generationHex: bytes.map { String(format: "%02x", $0) }.joined(), + createdAtUnixMilliseconds: max(1, UInt64(Date().timeIntervalSince1970 * 1000)) ) do { let data = try JSONEncoder.radroots.encode(value) @@ -417,29 +417,29 @@ actor RadrootsConfigurationStore { } private static func bootstrapConfiguration( - _ bootstrap: RadrootsConfigurationBootstrap, - profile: RadrootsAppNetworkProfile, - generation: UInt64, - activationState: RadrootsConfigurationActivationState, - bootstrapFingerprint: String, - previousBlossomConfigFingerprint: String? = nil + _ bootstrap: RadrootsConfigurationBootstrap, + profile: RadrootsAppNetworkProfile, + generation: UInt64, + activationState: RadrootsConfigurationActivationState, + bootstrapFingerprint: String, + previousBlossomConfigFingerprint: String? = nil ) -> StoredConfigurationV3 { StoredConfigurationV3( - format: StoredConfigurationV3.format, - profile: profile, - writableRelays: bootstrap.relayURLs, - blossom: blossomConfiguration(origins: bootstrap.blossomOrigins, profile: profile), - generation: generation, - activationState: activationState, - bootstrapFingerprint: bootstrapFingerprint, - canonicalBlossomConfigFingerprint: nil, - previousBlossomConfigFingerprint: previousBlossomConfigFingerprint + format: StoredConfigurationV3.format, + profile: profile, + writableRelays: bootstrap.relayURLs, + blossom: blossomConfiguration(origins: bootstrap.blossomOrigins, profile: profile), + generation: generation, + activationState: activationState, + bootstrapFingerprint: bootstrapFingerprint, + canonicalBlossomConfigFingerprint: nil, + previousBlossomConfigFingerprint: previousBlossomConfigFingerprint ) } private static func upgradeStoredConfiguration( - _ stored: StoredConfigurationV3, - bootstrapFingerprint: String + _ stored: StoredConfigurationV3, + bootstrapFingerprint: String ) throws -> StoredConfigurationV3 { if let generation = stored.generation, let activationState = stored.activationState, @@ -453,15 +453,15 @@ actor RadrootsConfigurationStore { throw RadrootsConfigurationError.corruptStoredConfiguration } return StoredConfigurationV3( - format: stored.format, - profile: stored.profile, - writableRelays: stored.writableRelays, - blossom: stored.blossom, - generation: generation, - activationState: activationState, - bootstrapFingerprint: persistedBootstrapFingerprint, - canonicalBlossomConfigFingerprint: stored.canonicalBlossomConfigFingerprint, - previousBlossomConfigFingerprint: stored.previousBlossomConfigFingerprint + format: stored.format, + profile: stored.profile, + writableRelays: stored.writableRelays, + blossom: stored.blossom, + generation: generation, + activationState: activationState, + bootstrapFingerprint: persistedBootstrapFingerprint, + canonicalBlossomConfigFingerprint: stored.canonicalBlossomConfigFingerprint, + previousBlossomConfigFingerprint: stored.previousBlossomConfigFingerprint ) } guard stored.generation == nil, @@ -473,28 +473,28 @@ actor RadrootsConfigurationStore { throw RadrootsConfigurationError.corruptStoredConfiguration } return StoredConfigurationV3( - format: stored.format, - profile: stored.profile, - writableRelays: stored.writableRelays, - blossom: stored.blossom, - generation: 1, - activationState: .current, - bootstrapFingerprint: bootstrapFingerprint, - canonicalBlossomConfigFingerprint: nil, - previousBlossomConfigFingerprint: nil + format: stored.format, + profile: stored.profile, + writableRelays: stored.writableRelays, + blossom: stored.blossom, + generation: 1, + activationState: .current, + bootstrapFingerprint: bootstrapFingerprint, + canonicalBlossomConfigFingerprint: nil, + previousBlossomConfigFingerprint: nil ) } private static func bootstrapFingerprint( - _ bootstrap: RadrootsConfigurationBootstrap, - profile: RadrootsAppNetworkProfile + _ bootstrap: RadrootsConfigurationBootstrap, + profile: RadrootsAppNetworkProfile ) throws -> String { do { let data = try JSONEncoder.radroots.encode( BootstrapNetworkIdentity( - profile: profile, - relayURLs: bootstrap.relayURLs, - blossomOrigins: bootstrap.blossomOrigins + profile: profile, + relayURLs: bootstrap.relayURLs, + blossomOrigins: bootstrap.blossomOrigins ) ) return SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined() @@ -510,22 +510,22 @@ actor RadrootsConfigurationStore { } private static func blossomConfiguration( - origins: [String], - profile: RadrootsAppNetworkProfile + origins: [String], + profile: RadrootsAppNetworkProfile ) -> RadrootsBlossomEndpointConfiguration? { guard let primary = origins.first else { return nil } let identity = blossomIdentity(profile: profile) return RadrootsBlossomEndpointConfiguration( - hostKind: identity.hostKind, - endpointAuthority: identity.endpointAuthority, - primaryOrigin: primary, - fallbackOrigins: Array(origins.dropFirst()) + hostKind: identity.hostKind, + endpointAuthority: identity.endpointAuthority, + primaryOrigin: primary, + fallbackOrigins: Array(origins.dropFirst()) ) } private static func blossomMatchesProfile( - _ configuration: RadrootsBlossomEndpointConfiguration?, - profile: RadrootsAppNetworkProfile + _ configuration: RadrootsBlossomEndpointConfiguration?, + profile: RadrootsAppNetworkProfile ) -> Bool { guard let configuration else { return true } let identity = blossomIdentity(profile: profile) @@ -536,8 +536,8 @@ actor RadrootsConfigurationStore { private static func blossomIdentity( profile: RadrootsAppNetworkProfile ) -> ( - hostKind: RadrootsBlossomHostKind, - endpointAuthority: RadrootsBlossomEndpointAuthority + hostKind: RadrootsBlossomHostKind, + endpointAuthority: RadrootsBlossomEndpointAuthority ) { switch profile { case .publicNetwork: @@ -554,8 +554,8 @@ enum RadrootsNetworkValidator { static let canonicalRelay = "wss://radroots.org" static func relays( - _ values: [String], - profile: RadrootsAppNetworkProfile + _ values: [String], + profile: RadrootsAppNetworkProfile ) throws -> [String] { var output: [String] = [] var seen = Set<String>() @@ -580,8 +580,8 @@ enum RadrootsNetworkValidator { } private static func relay( - _ raw: String, - profile: RadrootsAppNetworkProfile + _ raw: String, + profile: RadrootsAppNetworkProfile ) throws -> String { guard raw == raw.trimmingCharacters(in: .whitespacesAndNewlines), raw.utf8.count <= 2048, @@ -621,8 +621,8 @@ enum RadrootsNetworkValidator { } private static func hostAllowed( - _ host: String, - profile: RadrootsAppNetworkProfile + _ host: String, + profile: RadrootsAppNetworkProfile ) -> Bool { if profile == .simulator { return host == "localhost" || host == "127.0.0.1" || host == "::1" diff --git a/Radroots/State/RadrootsIdentityStore.swift b/Radroots/State/RadrootsIdentityStore.swift @@ -65,10 +65,10 @@ actor RadrootsIdentityStore { private let userDefaults: UserDefaults init( - custody: RadrootsIdentityCustody, - secureStore: any RadrootsSecureStore, - servicePrefix: String, - userDefaults: UserDefaults = .standard + custody: RadrootsIdentityCustody, + secureStore: any RadrootsSecureStore, + servicePrefix: String, + userDefaults: UserDefaults = .standard ) { self.custody = custody self.secureStore = secureStore @@ -78,9 +78,9 @@ actor RadrootsIdentityStore { @MainActor static func production( - servicePrefix: String, - protectedDataAvailable: @escaping @Sendable () -> Bool, - qualification: RadrootsRemoteQualificationEnvironment? = nil + servicePrefix: String, + protectedDataAvailable: @escaping @Sendable () -> Bool, + qualification: RadrootsRemoteQualificationEnvironment? = nil ) throws -> RadrootsIdentityStore { let namespace = "radroots_identity_v1" let secureStore = RadrootsAppleKeychainSecureStore(servicePrefix: servicePrefix) @@ -89,8 +89,8 @@ actor RadrootsIdentityStore { #if DEBUG if qualification != nil { configuration = try RadrootsIdentityCustodyConfiguration( - namespace: namespace, - secretPolicy: .secureLocalSecret + namespace: namespace, + secretPolicy: .secureLocalSecret ) userPresence = RadrootsRemoteQualificationUserPresence() } else { @@ -103,22 +103,22 @@ actor RadrootsIdentityStore { userPresence = RadrootsAppleUserPresence() #endif let custody = try RadrootsIdentityCustody( - configuration: configuration, - secureStore: secureStore, - metadataStore: RadrootsAppleIdentityMetadataStore( - namespace: namespace, - keyPrefix: qualification?.identityMetadataKeyPrefix + configuration: configuration, + secureStore: secureStore, + metadataStore: RadrootsAppleIdentityMetadataStore( + namespace: namespace, + keyPrefix: qualification?.identityMetadataKeyPrefix ?? "org.radroots.ios.identity" - ), - userPresence: userPresence, - protectedData: RadrootsProtectedDataProvider { + ), + userPresence: userPresence, + protectedData: RadrootsProtectedDataProvider { protectedDataAvailable() ? .available : .unavailable } ) return RadrootsIdentityStore( - custody: custody, - secureStore: secureStore, - servicePrefix: servicePrefix + custody: custody, + secureStore: secureStore, + servicePrefix: servicePrefix ) } @@ -129,8 +129,8 @@ actor RadrootsIdentityStore { } let legacySecretKey = RadrootsSecureStoreKey( - namespace: "nostr_identity", - name: "selected_secret_hex" + namespace: "nostr_identity", + name: "selected_secret_hex" ) let hasLegacySecret: Bool do { @@ -146,25 +146,25 @@ actor RadrootsIdentityStore { throw RadrootsIdentityStoreError.corruptLegacyMetadata } return RadrootsAppIdentity( - state: .recoveryRequired, - identityHandle: nil, - publicKeyHex: legacyMetadata?.publicKeyHex.lowercased(), - label: legacyMetadata?.label, - signerGeneration: nil, - recoveryCode: "identity.legacy_migration_required" + state: .recoveryRequired, + identityHandle: nil, + publicKeyHex: legacyMetadata?.publicKeyHex.lowercased(), + label: legacyMetadata?.label, + signerGeneration: nil, + recoveryCode: "identity.legacy_migration_required" ) } private func migrateLegacyIdentity() async throws -> RadrootsAppIdentity { let legacySecretKey = RadrootsSecureStoreKey( - namespace: "nostr_identity", - name: "selected_secret_hex" + namespace: "nostr_identity", + name: "selected_secret_hex" ) let legacyMetadata = try loadLegacyMetadata() do { let migrated = try await custody.migrateLegacyIdentity( - from: legacySecretKey, - label: legacyMetadata?.label + from: legacySecretKey, + label: legacyMetadata?.label ) if let metadata = legacyMetadata, metadata.publicKeyHex.lowercased() != migrated.identity?.publicKeyHex @@ -187,8 +187,8 @@ actor RadrootsIdentityStore { } func importIdentity( - _ material: RadrootsIdentitySecretMaterial, - label: String? = nil + _ material: RadrootsIdentitySecretMaterial, + label: String? = nil ) async throws -> RadrootsAppIdentity { try await custody.importIdentity(material, label: label).appValue } @@ -205,8 +205,8 @@ actor RadrootsIdentityStore { let snapshot = await custody.snapshot() if snapshot.state == .absent { let legacyKey = RadrootsSecureStoreKey( - namespace: "nostr_identity", - name: "selected_secret_hex" + namespace: "nostr_identity", + name: "selected_secret_hex" ) if try secureStore.contains(legacyKey) { return try await migrateLegacyIdentity() @@ -227,9 +227,9 @@ actor RadrootsIdentityStore { throw RadrootsIdentityStoreError.unavailable } return RadrootsAppleCustodySigner( - custody: custody, - signerHandle: signerHandle, - publicKeyHex: publicKeyHex + custody: custody, + signerHandle: signerHandle, + publicKeyHex: publicKeyHex ) } @@ -253,12 +253,12 @@ actor RadrootsIdentityStore { private static func appIdentity(_ snapshot: RadrootsIdentitySnapshot) -> RadrootsAppIdentity { RadrootsAppIdentity( - state: snapshot.state.appValue, - identityHandle: snapshot.identity?.identityHandle, - publicKeyHex: snapshot.identity?.publicKeyHex, - label: snapshot.identity?.label, - signerGeneration: snapshot.signerHandle, - recoveryCode: snapshot.recoveryCode + state: snapshot.state.appValue, + identityHandle: snapshot.identity?.identityHandle, + publicKeyHex: snapshot.identity?.publicKeyHex, + label: snapshot.identity?.label, + signerGeneration: snapshot.signerHandle, + recoveryCode: snapshot.recoveryCode ) } } @@ -295,12 +295,12 @@ private final class RadrootsAppleCustodySigner: RadrootsRuntimeSigner, @unchecke do { let result = try await custody.sign( RadrootsOpaqueSignRequest( - operationID: request.operationID, - signerHandle: signerHandle, - publicKeyHex: request.publicKeyHex, - digest: request.digest, - purpose: request.purpose.appleValue, - deadlineUnixMilliseconds: request.deadlineUnixMilliseconds + operationID: request.operationID, + signerHandle: signerHandle, + publicKeyHex: request.publicKeyHex, + digest: request.digest, + purpose: request.purpose.appleValue, + deadlineUnixMilliseconds: request.deadlineUnixMilliseconds ) ) return .signed(signatureHex: result.signature.map { String(format: "%02x", $0) }.joined()) @@ -328,12 +328,12 @@ private final class RadrootsAppleCustodySigner: RadrootsRuntimeSigner, @unchecke private extension RadrootsIdentitySnapshot { var appValue: RadrootsAppIdentity { RadrootsAppIdentity( - state: state.appValue, - identityHandle: identity?.identityHandle, - publicKeyHex: identity?.publicKeyHex, - label: identity?.label, - signerGeneration: signerHandle, - recoveryCode: recoveryCode + state: state.appValue, + identityHandle: identity?.identityHandle, + publicKeyHex: identity?.publicKeyHex, + label: identity?.label, + signerGeneration: signerHandle, + recoveryCode: recoveryCode ) } } diff --git a/Radroots/State/RadrootsMediaStore.swift b/Radroots/State/RadrootsMediaStore.swift @@ -153,7 +153,7 @@ final class RadrootsMediaStore: ObservableObject { private static func failureState(_ error: Error) -> RadrootsMediaPresentationState { let failure: RadrootsRuntimeFailure? = - if case RadrootsRuntimeClientError.support(let value) = error { + if case let RadrootsRuntimeClientError.support(value) = error { value } else { error as? RadrootsRuntimeFailure diff --git a/Radroots/State/RadrootsSessionStore.swift b/Radroots/State/RadrootsSessionStore.swift @@ -77,8 +77,8 @@ actor RadrootsSessionStore { ?? requiredString( "RADROOTS_FIELD_IOS_KEYCHAIN_SERVICE_PREFIX", bundle: bundle ) - let bootstrap = RadrootsConfigurationBootstrap( - runtimeMode: try qualification?.runtimeMode + let bootstrap = try RadrootsConfigurationBootstrap( + runtimeMode: qualification?.runtimeMode ?? requiredString("RADROOTS_FIELD_IOS_RUNTIME_MODE", bundle: bundle), relayURLs: qualification?.relayURLs ?? array("RADROOTS_FIELD_IOS_NOSTR_RELAY_URLS", bundle: bundle), diff --git a/Radroots/State/RadrootsSupportingStores.swift b/Radroots/State/RadrootsSupportingStores.swift @@ -20,8 +20,8 @@ final class RadrootsSearchStore: ObservableObject { private var generation: UInt64 = 0 init( - runtimeClient: RadrootsRuntimeClient, - now: @escaping @Sendable () -> UInt64 = { UInt64(Date().timeIntervalSince1970) } + runtimeClient: RadrootsRuntimeClient, + now: @escaping @Sendable () -> UInt64 = { UInt64(Date().timeIntervalSince1970) } ) { self.runtimeClient = runtimeClient self.now = now @@ -65,10 +65,10 @@ final class RadrootsSearchStore: ObservableObject { state = .loading do { let loaded = try await runtimeClient.search( - context: context, - query: normalized, - limit: 50, - asOfUnixSeconds: now() + context: context, + query: normalized, + limit: 50, + asOfUnixSeconds: now() ) guard requestedGeneration == generation, !Task.isCancelled else { return } results = Self.unique(loaded) @@ -114,9 +114,9 @@ final class RadrootsMeStore: ObservableObject { private var observationGeneration: UInt64 = 0 init( - runtimeClient: RadrootsRuntimeClient, - now: @escaping @Sendable () -> UInt64 = { UInt64(Date().timeIntervalSince1970) }, - observationDelay: @escaping @Sendable (UInt32) async throws -> Void = + runtimeClient: RadrootsRuntimeClient, + now: @escaping @Sendable () -> UInt64 = { UInt64(Date().timeIntervalSince1970) }, + observationDelay: @escaping @Sendable (UInt32) async throws -> Void = RadrootsRuntimeObservationBackoff.sleep ) { self.runtimeClient = runtimeClient @@ -161,8 +161,8 @@ final class RadrootsMeStore: ObservableObject { } do { let loaded = try await runtimeClient.me( - context: context, - asOfUnixSeconds: now() + context: context, + asOfUnixSeconds: now() ) guard requestedGeneration == generation, !Task.isCancelled else { return } snapshot = loaded @@ -318,25 +318,25 @@ final class RadrootsSettingsStore: ObservableObject { do { let transition = try await runtimeClient.replaceMobileSettings( input: RadrootsReplaceSettings( - expectedRevision: settings.revision, - networkEnvironment: networkEnvironment, - relays: relays, - blossomAuthority: blossomAuthority, - blossomPrimaryOrigin: blossomPrimaryOrigin, - blossomFallbackOrigins: fallbacks, - allowCellularDownloads: allowCellularDownloads, - allowCellularUploads: allowCellularUploads, - allowBackgroundTransfers: allowBackgroundTransfers, - mediaCacheBytes: UInt64(max(mediaCacheMegabytes, 1)) * 1_048_576, - mediaCacheArtifacts: UInt32(max(mediaCacheArtifacts, 1)) + expectedRevision: settings.revision, + networkEnvironment: networkEnvironment, + relays: relays, + blossomAuthority: blossomAuthority, + blossomPrimaryOrigin: blossomPrimaryOrigin, + blossomFallbackOrigins: fallbacks, + allowCellularDownloads: allowCellularDownloads, + allowCellularUploads: allowCellularUploads, + allowBackgroundTransfers: allowBackgroundTransfers, + mediaCacheBytes: UInt64(max(mediaCacheMegabytes, 1)) * 1_048_576, + mediaCacheArtifacts: UInt32(max(mediaCacheArtifacts, 1)) ) ) guard requestedGeneration == generation, !Task.isCancelled else { return false } apply(transition.settings) let effects = [ - transition.runtimeRestartRequired ? "runtime restart" : nil, - transition.outboxRequeueRequired ? "outbox requeue" : nil, - transition.mediaCacheInvalidationRequired ? "media cache refresh" : nil, + transition.runtimeRestartRequired ? "runtime restart" : nil, + transition.outboxRequeueRequired ? "outbox requeue" : nil, + transition.mediaCacheInvalidationRequired ? "media cache refresh" : nil, ].compactMap(\.self) message = effects.isEmpty ? "Settings saved." @@ -362,13 +362,13 @@ final class RadrootsSettingsStore: ObservableObject { do { let status = try await runtimeClient.saveProfileMetadata( input: RadrootsProfileMetadataInput( - name: profileName, - displayName: optional(profileDisplayName), - about: optional(profileAbout), - picture: nil, - banner: nil, - nip05: optional(profileNip05), - bot: profileBot + name: profileName, + displayName: optional(profileDisplayName), + about: optional(profileAbout), + picture: nil, + banner: nil, + nip05: optional(profileNip05), + bot: profileBot ) ) guard requestedGeneration == generation, !Task.isCancelled else { return } @@ -392,8 +392,8 @@ final class RadrootsSettingsStore: ObservableObject { guard let profileStatus else { return } await runProfileOperation { try await self.runtimeClient.cancelProfile( - operationID: profileStatus.id, - expectedRevision: profileStatus.revision + operationID: profileStatus.id, + expectedRevision: profileStatus.revision ) } } diff --git a/Radroots/State/RadrootsTodayStore.swift b/Radroots/State/RadrootsTodayStore.swift @@ -31,14 +31,14 @@ final class RadrootsTodayStore: ObservableObject { private var observationGeneration: UInt64 = 0 init( - runtimeClient: RadrootsRuntimeClient, - contexts: [RadrootsLocalNetwork] = [], - selectedContextID: String? = nil, - pageSize: UInt16 = 20, - now: @escaping @Sendable () -> UInt64 = { + runtimeClient: RadrootsRuntimeClient, + contexts: [RadrootsLocalNetwork] = [], + selectedContextID: String? = nil, + pageSize: UInt16 = 20, + now: @escaping @Sendable () -> UInt64 = { UInt64(Date().timeIntervalSince1970) }, - observationDelay: @escaping @Sendable (UInt32) async throws -> Void = + observationDelay: @escaping @Sendable (UInt32) async throws -> Void = RadrootsRuntimeObservationBackoff.sleep ) { self.runtimeClient = runtimeClient @@ -128,8 +128,8 @@ final class RadrootsTodayStore: ObservableObject { } func reload( - refreshProjection: Bool = true, - update: RadrootsTodayProjectionUpdate = .incremental + refreshProjection: Bool = true, + update: RadrootsTodayProjectionUpdate = .incremental ) async { guard let context = selectedContext else { cards = [] @@ -150,9 +150,9 @@ final class RadrootsTodayStore: ObservableObject { if refreshProjection { do { _ = try await runtimeClient.refreshToday( - context: context, - nowUnixSeconds: now(), - update: update + context: context, + nowUnixSeconds: now(), + update: update ) } catch { refreshFailure = error @@ -163,9 +163,9 @@ final class RadrootsTodayStore: ObservableObject { let asOf = now() let page = try await runtimeClient.todayPage( request: .first( - context: context, - limit: pageSize, - asOfUnixSeconds: asOf + context: context, + limit: pageSize, + asOfUnixSeconds: asOf ) ) guard generation == requestGeneration, !Task.isCancelled else { return } diff --git a/Radroots/Views/RadrootsAddView.swift b/Radroots/Views/RadrootsAddView.swift @@ -26,7 +26,7 @@ struct RadrootsAddView: View { ProgressView("Loading Add…") .accessibilityIdentifier("radroots.add.loading") } - case .failed(let message): + case let .failed(message): Section { Label(message, systemImage: "exclamationmark.triangle") .foregroundStyle(.secondary) @@ -265,7 +265,8 @@ struct RadrootsAddView: View { .accessibilityValue( "\(media.width) by \(media.height), " + ByteCountFormatter.string( - fromByteCount: Int64(clamping: media.byteSize), countStyle: .file) + fromByteCount: Int64(clamping: media.byteSize), countStyle: .file + ) ) } diff --git a/Radroots/Views/RadrootsSupportingViews.swift b/Radroots/Views/RadrootsSupportingViews.swift @@ -24,7 +24,7 @@ struct RadrootsSearchSheet: View { ProgressView("Searching…") case .empty: ContentUnavailableView.search(text: store.query) - case .failed(let message): + case let .failed(message): ContentUnavailableView { Label("Search unavailable", systemImage: "exclamationmark.triangle") } description: { @@ -52,7 +52,7 @@ struct RadrootsSearchSheet: View { private var resultList: some View { List(store.results) { result in switch (result.card, result.profile) { - case (let card?, _): + case let (card?, _): NavigationLink { RadrootsTodayDetailView( card: card, @@ -68,7 +68,7 @@ struct RadrootsSearchSheet: View { RadrootsTodayCardView(card: card, context: context, mediaStore: mediaStore) } .accessibilityIdentifier("radroots.search.card.\(result.id)") - case (_, let profile?): + case let (_, profile?): NavigationLink { RadrootsProfileView(profile: profile, context: context, mediaStore: mediaStore) } label: { @@ -110,7 +110,7 @@ struct RadrootsMeSheet: View { switch store.state { case .idle, .loading: ProgressView("Loading your profile…") - case .failed(let message) where store.snapshot == nil: + case let .failed(message) where store.snapshot == nil: ContentUnavailableView { Label("Profile unavailable", systemImage: "person.crop.circle.badge.exclamationmark") } description: { @@ -408,7 +408,7 @@ struct RadrootsSettingsView: View { TextField("Display name", text: $settingsStore.profileDisplayName) .accessibilityIdentifier("radroots.settings.profile.display_name") TextField("About", text: $settingsStore.profileAbout, axis: .vertical) - .lineLimit(3...8) + .lineLimit(3 ... 8) .accessibilityIdentifier("radroots.settings.profile.about") TextField("NIP-05 identifier", text: $settingsStore.profileNip05) .textInputAutocapitalization(.never) @@ -488,7 +488,7 @@ struct RadrootsSettingsView: View { text: $settingsStore.blossomFallbackOrigins, axis: .vertical ) - .lineLimit(2...5) + .lineLimit(2 ... 5) .textInputAutocapitalization(.never) .keyboardType(.URL) Toggle("Cellular downloads", isOn: $settingsStore.allowCellularDownloads) @@ -499,13 +499,13 @@ struct RadrootsSettingsView: View { Stepper( "Cache: \(settingsStore.mediaCacheMegabytes) MB", value: $settingsStore.mediaCacheMegabytes, - in: 16...2048, + in: 16 ... 2048, step: 16 ) Stepper( "Artifacts: \(settingsStore.mediaCacheArtifacts)", value: $settingsStore.mediaCacheArtifacts, - in: 1...10000, + in: 1 ... 10000, step: 100 ) Button("Save network and storage settings") { diff --git a/Radroots/Views/RadrootsTodayView.swift b/Radroots/Views/RadrootsTodayView.swift @@ -24,14 +24,14 @@ struct RadrootsTodayView: View { .accessibilityIdentifier("radroots.today.loading") case .empty: emptyView - case .failed(let message) where store.cards.isEmpty: + case let .failed(message) where store.cards.isEmpty: unavailableView( title: "Today is unavailable", message: message, systemImage: "exclamationmark.triangle" ) .accessibilityIdentifier("radroots.today.error") - case .offline(let message) where store.cards.isEmpty: + case let .offline(message) where store.cards.isEmpty: unavailableView( title: "You’re offline", message: message, @@ -99,9 +99,9 @@ struct RadrootsTodayView: View { private var feed: some View { List { - if case .offline(let message) = store.state { + if case let .offline(message) = store.state { statusBanner(message: message, systemImage: "wifi.slash") - } else if case .failed(let message) = store.state { + } else if case let .failed(message) = store.state { statusBanner(message: message, systemImage: "exclamationmark.triangle") } @@ -375,7 +375,7 @@ struct RadrootsLocalMediaContent: View { let state = store.state(for: media, context: context) Group { switch state { - case .ready(let artifact): + case let .ready(artifact): if let image = UIImage(data: artifact.bytes) { Image(uiImage: image) .resizable() diff --git a/Radroots/Views/RuntimeStatusView.swift b/Radroots/Views/RuntimeStatusView.swift @@ -196,9 +196,9 @@ private struct RadrootsSecureIdentityImportField: UIViewRepresentable { let container = UIView() container.addSubview(stack) NSLayoutConstraint.activate([ - stack.leadingAnchor.constraint(equalTo: container.leadingAnchor), - stack.trailingAnchor.constraint(equalTo: container.trailingAnchor), - stack.topAnchor.constraint(equalTo: container.topAnchor), + stack.leadingAnchor.constraint(equalTo: container.leadingAnchor), + stack.trailingAnchor.constraint(equalTo: container.trailingAnchor), + stack.topAnchor.constraint(equalTo: container.topAnchor), ]) context.coordinator.field = field context.coordinator.errorLabel = error diff --git a/RadrootsTests/RadrootsAddStoreTests.swift b/RadrootsTests/RadrootsAddStoreTests.swift @@ -1,9 +1,8 @@ import CryptoKit +@testable import RadrootsApp import RadrootsKit import XCTest -@testable import RadrootsApp - final class RadrootsAddStoreTests: XCTestCase { func testBackgroundUploadResumesReceiptRetriesAndCompletedStateWithoutDuplicateEnqueue() async throws @@ -389,7 +388,7 @@ final class RadrootsAddStoreTests: XCTestCase { let recordedRetraction = await backend.lastRetraction() let retraction = try XCTUnwrap(recordedRetraction) XCTAssertEqual(retraction.commandType, .createFoodAvailability) - XCTAssertEqual(retraction.targetKind, 30_402) + XCTAssertEqual(retraction.targetKind, 30402) XCTAssertEqual(retraction.targetCardID, String(repeating: "c", count: 64)) XCTAssertEqual(retraction.targetEventID, String(repeating: "e", count: 64)) XCTAssertEqual(retraction.targetAddress, Self.card().sourceAddress) @@ -545,7 +544,7 @@ final class RadrootsAddStoreTests: XCTestCase { await store.start() store.selectType(.createPhotoUpdate) - for _ in 0..<20 { + for _ in 0 ..< 20 { await store.importPhotos() } XCTAssertEqual(store.form.media.count, 20) @@ -664,8 +663,10 @@ final class RadrootsAddStoreTests: XCTestCase { private static func waitUntil( _ predicate: @escaping @Sendable () async -> Bool ) async -> Bool { - for _ in 0..<1_000 { - if await predicate() { return true } + for _ in 0 ..< 1000 { + if await predicate() { + return true + } try? await Task.sleep(nanoseconds: 1_000_000) } return false @@ -897,7 +898,7 @@ private actor AddBackend: RadrootsRuntimeBackend { writeLastAttemptUnixMilliseconds: nil, readNextAttemptUnixMilliseconds: nil, writeNextAttemptUnixMilliseconds: nil - ) + ), ] : [] ), blossomConfiguration: RadrootsBlossomConfigurationStatus( @@ -965,14 +966,14 @@ private actor AddBackend: RadrootsRuntimeBackend { schemaVersion: 1, commandType: .createUpdate, label: "Update", - fields: [text("content", "Update", .multilineText, true, 65_535)] + fields: [text("content", "Update", .multilineText, true, 65535)] ), RadrootsAddSchema( schemaVersion: 1, commandType: .createPhotoUpdate, label: "Photo update", fields: [ - text("content", "Update", .multilineText, true, 65_535), + text("content", "Update", .multilineText, true, 65535), media(true, 20), ] ), @@ -981,7 +982,7 @@ private actor AddBackend: RadrootsRuntimeBackend { commandType: .createAsk, label: "Ask", fields: [ - text("content", "Question", .multilineText, true, 65_535), + text("content", "Question", .multilineText, true, 65535), media(false, 20), ] ), @@ -992,7 +993,7 @@ private actor AddBackend: RadrootsRuntimeBackend { fields: [ text("identifier", "Identifier", .text, true, 256), text("title", "Title", .text, true, 256), - text("content", "Description", .multilineText, false, 65_535), + text("content", "Description", .multilineText, false, 65535), text("event_start", "Starts", .dateTime, true, nil), text("event_end", "Ends", .dateTime, false, nil), text("location", "Location", .location, false, 256), @@ -1007,7 +1008,7 @@ private actor AddBackend: RadrootsRuntimeBackend { text("identifier", "Identifier", .text, true, 256), text("title", "Food", .text, true, 256), text("summary", "Summary", .text, true, 256), - text("content", "Details", .multilineText, true, 65_535), + text("content", "Details", .multilineText, true, 65535), text("location", "Pickup location", .location, true, 256), text("price_amount", "Price", .decimal, true, 64), field("currency", "Currency", .choice, true, [], 3, nil), @@ -1213,7 +1214,8 @@ private actor AddBackend: RadrootsRuntimeBackend { guard current.revision == expectedRevision else { throw unsupported() } let value = replacing( current, revision: current.revision + 1, state: .queued, - updatedAt: current.updatedAtUnixMilliseconds + 1) + updatedAt: current.updatedAtUnixMilliseconds + 1 + ) values[id] = value return value } @@ -1222,7 +1224,8 @@ private actor AddBackend: RadrootsRuntimeBackend { let current = try draftStatus(id: id) let value = replacing( current, revision: current.revision + 1, state: .queued, - updatedAt: current.updatedAtUnixMilliseconds + 1) + updatedAt: current.updatedAtUnixMilliseconds + 1 + ) values[id] = value return value } @@ -1334,7 +1337,8 @@ private actor AddBackend: RadrootsRuntimeBackend { let current = try storedDraft(id: id) let value = replacing( current, revision: expectedRevision, state: .complete, - updatedAt: current.updatedAtUnixMilliseconds) + updatedAt: current.updatedAtUnixMilliseconds + ) values[id] = value return value } @@ -1346,7 +1350,8 @@ private actor AddBackend: RadrootsRuntimeBackend { let current = try draftStatus(id: id) let value = replacing( current, revision: current.revision + 1, state: .cancelled, - updatedAt: current.updatedAtUnixMilliseconds + 1) + updatedAt: current.updatedAtUnixMilliseconds + 1 + ) values[id] = value return value } @@ -1451,7 +1456,8 @@ private actor AddBackend: RadrootsRuntimeBackend { private func unsupported() -> RadrootsRuntimeFailure { .local( - operation: "test.add", code: "test.unsupported", safeMessage: "Unsupported test operation.") + operation: "test.add", code: "test.unsupported", safeMessage: "Unsupported test operation." + ) } } @@ -1544,7 +1550,7 @@ private final class BackgroundUploadFixture: @unchecked Sendable { possibleOrphan: false, orphanReasonCode: nil, orphanRecordedAtUnixMilliseconds: nil - ) + ), ], settlement: nil, isRevision: false @@ -1698,7 +1704,7 @@ private actor BackgroundTransferHarness: RadrootsBackgroundTransfer { case .accepted: acceptedSettlementCount += 1 values[identifier] = try snapshot(request: value.request, state: .completed) - case .rejected(let code): + case let .rejected(code): values[identifier] = try RadrootsBackgroundTransferSnapshot( request: value.request, state: .failed, diff --git a/RadrootsTests/RadrootsLifecycleTests.swift b/RadrootsTests/RadrootsLifecycleTests.swift @@ -1,9 +1,8 @@ import Foundation +@testable import RadrootsApp import RadrootsKit import XCTest -@testable import RadrootsApp - final class RadrootsLifecycleTests: XCTestCase { func testBackgroundEventsCompleteExactlyOnceBeforeAndAfterAttachment() async { let router = RadrootsBackgroundEventRouter() @@ -31,7 +30,7 @@ final class RadrootsLifecycleTests: XCTestCase { func testBackgroundEventQueueIsBoundedAndShutdownCompletesPendingEvents() async { let router = RadrootsBackgroundEventRouter() - let probes = (0..<12).map { _ in CompletionProbe() } + let probes = (0 ..< 12).map { _ in CompletionProbe() } for probe in probes { await router.handle( @@ -73,7 +72,7 @@ final class RadrootsLifecycleTests: XCTestCase { let privateKey = String(repeating: "ab", count: 32) let privatePath = "/Users/private/radroots/private.json" - for index in 0..<40 { + for index in 0 ..< 40 { await coordinator.record( "ios.lifecycle.test_event", fields: [ @@ -111,7 +110,8 @@ final class RadrootsLifecycleTests: XCTestCase { private func makeRoots() throws -> RadrootsAppleFileRoots { let base = FileManager.default.temporaryDirectory .appendingPathComponent( - "radroots-ios-lifecycle-tests-\(UUID().uuidString)", isDirectory: true) + "radroots-ios-lifecycle-tests-\(UUID().uuidString)", isDirectory: true + ) return try RadrootsAppleFileRoots( appIdentifier: "org.radroots.tests", dataRoot: base.appendingPathComponent("data", isDirectory: true), @@ -141,7 +141,7 @@ final class RadrootsLifecycleTests: XCTestCase { writeLastAttemptUnixMilliseconds: nil, readNextAttemptUnixMilliseconds: nil, writeNextAttemptUnixMilliseconds: nil - ) + ), ] ), blossomConfiguration: nil, diff --git a/RadrootsTests/RadrootsMediaStoreTests.swift b/RadrootsTests/RadrootsMediaStoreTests.swift @@ -44,14 +44,14 @@ final class RadrootsMediaStoreTests: XCTestCase { func testRetryableRetrievalFailurePresentsOfflineState() async throws { let backend = MediaBackend( retrievalFailure: RadrootsRuntimeFailure( - schemaVersion: 1, - code: "media_network_offline", - category: "network", - retryable: true, - recoveryActions: ["retry"], - operationID: "test.media.retrieve", - capabilityID: "blossom_source", - safeMessage: "The photo service is offline." + schemaVersion: 1, + code: "media_network_offline", + category: "network", + retryable: true, + recoveryActions: ["retry"], + operationID: "test.media.retrieve", + capabilityID: "blossom_source", + safeMessage: "The photo service is offline." ) ) let client = try await startedClient(backend: backend) @@ -95,8 +95,8 @@ final class RadrootsMediaStoreTests: XCTestCase { @MainActor private func waitUntil( - attempts: Int = 100, - condition: () -> Bool + attempts: Int = 100, + condition: () -> Bool ) async { for _ in 0 ..< attempts { if condition() { @@ -110,40 +110,40 @@ final class RadrootsMediaStoreTests: XCTestCase { private func makeConfiguration() -> RadrootsRuntimeLaunchConfiguration { RadrootsRuntimeLaunchConfiguration( - applicationSupportDirectory: "/tmp/radroots-media-tests", - publicKeyHex: String(repeating: "ab", count: 32), - sourceGenerationHex: String(repeating: "cd", count: 32), - sourceGenerationCreatedAtUnixMilliseconds: 1, - protectedData: .available, - networkProfile: .simulator, - writableRelays: ["ws://127.0.0.1:21000"], - blossom: RadrootsBlossomEndpointConfiguration( - hostKind: .simulator, - endpointAuthority: .loopbackDevelopment, - primaryOrigin: "http://127.0.0.1:21100", - fallbackOrigins: [] - ), - app: RadrootsRuntimeAppMetadata( - bundleIdentifier: "org.radroots.media-tests", - version: "0.1.0-alpha", - buildNumber: "1", - buildSHA: nil - ), - signerGeneration: "media-tests", - signer: MediaTestSigner(), - adoptBootstrapSettings: false + applicationSupportDirectory: "/tmp/radroots-media-tests", + publicKeyHex: String(repeating: "ab", count: 32), + sourceGenerationHex: String(repeating: "cd", count: 32), + sourceGenerationCreatedAtUnixMilliseconds: 1, + protectedData: .available, + networkProfile: .simulator, + writableRelays: ["ws://127.0.0.1:21000"], + blossom: RadrootsBlossomEndpointConfiguration( + hostKind: .simulator, + endpointAuthority: .loopbackDevelopment, + primaryOrigin: "http://127.0.0.1:21100", + fallbackOrigins: [] + ), + app: RadrootsRuntimeAppMetadata( + bundleIdentifier: "org.radroots.media-tests", + version: "0.1.0-alpha", + buildNumber: "1", + buildSHA: nil + ), + signerGeneration: "media-tests", + signer: MediaTestSigner(), + adoptBootstrapSettings: false ) } private func makeContext() -> RadrootsLocalNetwork { RadrootsLocalNetwork( - schemaVersion: 1, - id: "media-context", - label: "Media context", - relayURLs: ["ws://127.0.0.1:21000"], - locality: nil, - followedAuthors: [], - generation: 1 + schemaVersion: 1, + id: "media-context", + label: "Media context", + relayURLs: ["ws://127.0.0.1:21000"], + locality: nil, + followedAuthors: [], + generation: 1 ) } @@ -151,15 +151,15 @@ final class RadrootsMediaStoreTests: XCTestCase { verification: RadrootsMediaVerificationState ) -> RadrootsMediaReference { RadrootsMediaReference( - referenceFingerprint: String(repeating: "f", count: 64), - url: "https://blossom.example/\(String(repeating: "a", count: 64)).png", - sha256: String(repeating: "a", count: 64), - mediaType: "image/png", - width: 1, - height: 1, - byteSize: nil, - alt: "A farm photo", - verification: verification + referenceFingerprint: String(repeating: "f", count: 64), + url: "https://blossom.example/\(String(repeating: "a", count: 64)).png", + sha256: String(repeating: "a", count: 64), + mediaType: "image/png", + width: 1, + height: 1, + byteSize: nil, + alt: "A farm photo", + verification: verification ) } @@ -169,12 +169,12 @@ final class RadrootsMediaStoreTests: XCTestCase { )! ) -> RadrootsVerifiedMediaArtifact { RadrootsVerifiedMediaArtifact( - artifactID: String(repeating: "a", count: 64), - bytes: bytes, - byteSize: UInt64(bytes.count), - mediaType: "image/png", - width: 1, - height: 1 + artifactID: String(repeating: "a", count: 64), + bytes: bytes, + byteSize: UInt64(bytes.count), + mediaType: "image/png", + width: 1, + height: 1 )! } } @@ -199,9 +199,9 @@ private actor MediaBackend: RadrootsRuntimeBackend { private var closed = false init( - cachedArtifact: RadrootsVerifiedMediaArtifact? = nil, - retrievedArtifact: RadrootsVerifiedMediaArtifact? = nil, - retrievalFailure: RadrootsRuntimeFailure? = nil + cachedArtifact: RadrootsVerifiedMediaArtifact? = nil, + retrievedArtifact: RadrootsVerifiedMediaArtifact? = nil, + retrievalFailure: RadrootsRuntimeFailure? = nil ) { self.cachedArtifact = cachedArtifact self.retrievedArtifact = retrievedArtifact @@ -210,16 +210,16 @@ private actor MediaBackend: RadrootsRuntimeBackend { func snapshot() -> RadrootsRuntimeSnapshot { RadrootsRuntimeSnapshot( - identity: RadrootsRuntimeIdentity( - publicKeyHex: String(repeating: "ab", count: 32), - hostSignerConfigured: true - ), - relay: nil, - blossomConfiguration: nil, - blossomEvidence: nil, - crateName: "radroots_mobile_ffi", - crateVersion: "0.1.0-alpha", - isClosed: closed + identity: RadrootsRuntimeIdentity( + publicKeyHex: String(repeating: "ab", count: 32), + hostSignerConfigured: true + ), + relay: nil, + blossomConfiguration: nil, + blossomEvidence: nil, + crateName: "radroots_mobile_ffi", + crateVersion: "0.1.0-alpha", + isClosed: closed ) } @@ -228,16 +228,16 @@ private actor MediaBackend: RadrootsRuntimeBackend { } func refreshToday( - context _: RadrootsLocalNetwork, - nowUnixSeconds _: UInt64, - update _: RadrootsTodayProjectionUpdate + context _: RadrootsLocalNetwork, + nowUnixSeconds _: UInt64, + update _: RadrootsTodayProjectionUpdate ) throws -> RadrootsTodayRefreshReceipt { throw testFailure(operation: "test.media.refresh") } func retrieveMedia( - context _: RadrootsLocalNetwork, - reference _: RadrootsMediaReference + context _: RadrootsLocalNetwork, + reference _: RadrootsMediaReference ) throws -> RadrootsVerifiedMediaArtifact { retrieveCalls += 1 if let retrievalFailure { @@ -250,16 +250,16 @@ private actor MediaBackend: RadrootsRuntimeBackend { } func verifiedMediaArtifact( - context _: RadrootsLocalNetwork, - artifactID _: String + context _: RadrootsLocalNetwork, + artifactID _: String ) -> RadrootsVerifiedMediaArtifact? { cachedCalls += 1 return cachedArtifact } func invalidateMediaArtifact( - context _: RadrootsLocalNetwork, - artifactID: String + context _: RadrootsLocalNetwork, + artifactID: String ) -> Bool { invalidatedIDs.append(artifactID) cachedArtifact = nil @@ -267,8 +267,8 @@ private actor MediaBackend: RadrootsRuntimeBackend { } func subscribe( - bufferCapacity _: Int, - receive _: @escaping @Sendable (RadrootsRuntimeChange) async -> Void + bufferCapacity _: Int, + receive _: @escaping @Sendable (RadrootsRuntimeChange) async -> Void ) -> any RadrootsRuntimeSubscriptionToken { MediaSubscriptionToken() } @@ -289,9 +289,9 @@ private actor MediaBackend: RadrootsRuntimeBackend { private func testFailure(operation: String) -> RadrootsRuntimeFailure { .local( - operation: operation, - code: "test.media.unsupported", - safeMessage: "This test operation is unsupported." + operation: operation, + code: "test.media.unsupported", + safeMessage: "This test operation is unsupported." ) } } diff --git a/RadrootsTests/RadrootsRemoteQualificationTests.swift b/RadrootsTests/RadrootsRemoteQualificationTests.swift @@ -1,9 +1,8 @@ import CryptoKit +@testable import RadrootsApp import RadrootsKit import XCTest -@testable import RadrootsApp - final class RadrootsRemoteQualificationTests: XCTestCase { func testQualificationIsOptInAndCarriesOnlyPublicInputs() throws { XCTAssertNil(try RadrootsRemoteQualificationEnvironment.current(environment: [:])) @@ -48,14 +47,14 @@ final class RadrootsRemoteQualificationTests: XCTestCase { XCTAssertThrowsError( try RadrootsRemoteQualificationEnvironment.current( environment: base.merging([ - RadrootsRemoteQualificationEnvironment.runIDKey: "../../bad" + RadrootsRemoteQualificationEnvironment.runIDKey: "../../bad", ]) { _, new in new } ) ) XCTAssertThrowsError( try RadrootsRemoteQualificationEnvironment.current( environment: base.merging([ - RadrootsRemoteQualificationEnvironment.networkProfileKey: "automatic" + RadrootsRemoteQualificationEnvironment.networkProfileKey: "automatic", ]) { _, new in new } ) ) @@ -84,7 +83,7 @@ final class RadrootsRemoteQualificationTests: XCTestCase { try RadrootsRemoteQualificationEnvironment.current( environment: base.merging([ RadrootsRemoteQualificationEnvironment.blossomOriginsKey: - "https://one.example,https://two.example" + "https://one.example,https://two.example", ]) { _, new in new } ) ) @@ -92,7 +91,7 @@ final class RadrootsRemoteQualificationTests: XCTestCase { try RadrootsRemoteQualificationEnvironment.current( environment: base.merging([ RadrootsRemoteQualificationEnvironment.mediaRelativePathKey: - "../../private-key" + "../../private-key", ]) { _, new in new } ) ) @@ -139,8 +138,8 @@ final class RadrootsRemoteQualificationTests: XCTestCase { } func testQualificationMediaFixtureHasPinnedDigest() throws { - let digest = SHA256.hash( - data: try RadrootsRemoteQualificationEnvironment.mediaFixtureData() + let digest = try SHA256.hash( + data: RadrootsRemoteQualificationEnvironment.mediaFixtureData() ) XCTAssertEqual( digest.map { String(format: "%02x", $0) }.joined(), diff --git a/RadrootsTests/RadrootsRootShellTests.swift b/RadrootsTests/RadrootsRootShellTests.swift @@ -15,58 +15,58 @@ final class RadrootsRootShellTests: XCTestCase { func testDeepLinksAcceptOnlyCanonicalRootAuthorities() throws { for (rawValue, expected) in [ - ("radroots://today", RadrootsRootTab.today), - ("RADROOTS://TODAY", RadrootsRootTab.today), - ("radroots://add", RadrootsRootTab.add), - ("RadRoots://AdD", RadrootsRootTab.add), + ("radroots://today", RadrootsRootTab.today), + ("RADROOTS://TODAY", RadrootsRootTab.today), + ("radroots://add", RadrootsRootTab.add), + ("RadRoots://AdD", RadrootsRootTab.add), ] { XCTAssertEqual( - try RadrootsRootTab.resolve(url: XCTUnwrap(URL(string: rawValue))), - expected, - "Expected canonical root deep link: \(rawValue)" + try RadrootsRootTab.resolve(url: XCTUnwrap(URL(string: rawValue))), + expected, + "Expected canonical root deep link: \(rawValue)" ) } } func testDeepLinksRejectRemovedAndStructurallyAmbiguousRoutes() throws { for rawValue in [ - "radroots://capture", - "radroots://activity", - "radroots://settings", - "radroots://search", - "radroots://me", - "radroots:today", - "radroots:/today", - "radroots:///today", - "radroots://host/today", - "radroots://today/", - "radroots://today/add", - "radroots://add/today", + "radroots://capture", + "radroots://activity", + "radroots://settings", + "radroots://search", + "radroots://me", + "radroots:today", + "radroots:/today", + "radroots:///today", + "radroots://host/today", + "radroots://today/", + "radroots://today/add", + "radroots://add/today", ] { XCTAssertNil( - try RadrootsRootTab.resolve(url: XCTUnwrap(URL(string: rawValue))), - "Expected noncanonical deep link rejection: \(rawValue)" + try RadrootsRootTab.resolve(url: XCTUnwrap(URL(string: rawValue))), + "Expected noncanonical deep link rejection: \(rawValue)" ) } } - func testDeepLinksRejectAuthorityDecorationsAndEncodedAliases() throws { + func testDeepLinksRejectAuthorityDecorationsAndEncodedAliases() { for rawValue in [ - "radroots://user@today", - "radroots://user:password@add", - "radroots://today:7447", - "radroots://today?", - "radroots://today?source=widget", - "radroots://today#", - "radroots://add#composer", - "radroots://%74oday", - "radroots://%61dd", - "radroots://today/%61dd", + "radroots://user@today", + "radroots://user:password@add", + "radroots://today:7447", + "radroots://today?", + "radroots://today?source=widget", + "radroots://today#", + "radroots://add#composer", + "radroots://%74oday", + "radroots://%61dd", + "radroots://today/%61dd", ] { if let url = URL(string: rawValue) { XCTAssertNil( - RadrootsRootTab.resolve(url: url), - "Expected decorated or encoded deep link rejection: \(rawValue)" + RadrootsRootTab.resolve(url: url), + "Expected decorated or encoded deep link rejection: \(rawValue)" ) } } diff --git a/RadrootsTests/RadrootsRuntimeClientTests.swift b/RadrootsTests/RadrootsRuntimeClientTests.swift @@ -41,10 +41,10 @@ final class RadrootsRuntimeClientTests: XCTestCase { $0?.generation } let secondValues = await [ - secondIterator.next(), - secondIterator.next(), - secondIterator.next(), - secondIterator.next(), + secondIterator.next(), + secondIterator.next(), + secondIterator.next(), + secondIterator.next(), ].compactMap { $0?.generation } XCTAssertEqual(firstValues, [9, 10]) @@ -56,9 +56,9 @@ final class RadrootsRuntimeClientTests: XCTestCase { func testOverlappingStopsAwaitOneTypedShutdownFailure() async throws { let failure = RadrootsRuntimeFailure.local( - operation: "test.shutdown", - code: "test.shutdown_failed", - safeMessage: "Shutdown did not complete." + operation: "test.shutdown", + code: "test.shutdown_failed", + safeMessage: "Shutdown did not complete." ) let harness = RuntimeHarness(shutdownFailure: failure) let client = RadrootsRuntimeClient(factory: harness.start) @@ -145,8 +145,8 @@ final class RadrootsRuntimeClientTests: XCTestCase { func testStartupDeadlineReturnsPromptlyAndClosesLateBackend() async throws { let harness = RuntimeHarness(startDelayNanoseconds: 60_000_000) let client = RadrootsRuntimeClient( - factory: harness.start, - deadlines: makeDeadlines(startup: 5_000_000) + factory: harness.start, + deadlines: makeDeadlines(startup: 5_000_000) ) do { @@ -217,8 +217,8 @@ final class RadrootsRuntimeClientTests: XCTestCase { func testOperationDeadlineAndLateValueCannotEscape() async throws { let harness = RuntimeHarness(snapshotDelayNanoseconds: 60_000_000) let client = RadrootsRuntimeClient( - factory: harness.start, - deadlines: makeDeadlines(operation: 5_000_000) + factory: harness.start, + deadlines: makeDeadlines(operation: 5_000_000) ) _ = try await client.start(configuration: makeConfiguration(generation: "09")) @@ -262,8 +262,8 @@ final class RadrootsRuntimeClientTests: XCTestCase { func testShutdownDeadlineQuarantinesBackendUntilLateSuccess() async throws { let harness = RuntimeHarness(shutdownDelayNanoseconds: 60_000_000) let client = RadrootsRuntimeClient( - factory: harness.start, - deadlines: makeDeadlines(shutdown: 5_000_000) + factory: harness.start, + deadlines: makeDeadlines(shutdown: 5_000_000) ) _ = try await client.start(configuration: makeConfiguration(generation: "0a")) @@ -292,9 +292,9 @@ final class RadrootsRuntimeClientTests: XCTestCase { let client = RadrootsRuntimeClient(factory: harness.start) let snapshot = try await client.start(configuration: makeConfiguration(generation: "0b")) let store = RadrootsTodayStore( - runtimeClient: client, - contexts: [.defaultContext(snapshot: snapshot)], - observationDelay: { _ in try await Task.sleep(nanoseconds: 20_000_000) } + runtimeClient: client, + contexts: [.defaultContext(snapshot: snapshot)], + observationDelay: { _ in try await Task.sleep(nanoseconds: 20_000_000) } ) await store.start() @@ -314,16 +314,16 @@ final class RadrootsRuntimeClientTests: XCTestCase { } private func makeDeadlines( - startup: UInt64 = 1_000_000_000, - operation: UInt64 = 1_000_000_000, - subscription: UInt64 = 1_000_000_000, - shutdown: UInt64 = 1_000_000_000 + startup: UInt64 = 1_000_000_000, + operation: UInt64 = 1_000_000_000, + subscription: UInt64 = 1_000_000_000, + shutdown: UInt64 = 1_000_000_000 ) -> RadrootsRuntimeDeadlinePolicy { RadrootsRuntimeDeadlinePolicy( - startupNanoseconds: startup, - operationNanoseconds: operation, - subscriptionNanoseconds: subscription, - shutdownNanoseconds: shutdown + startupNanoseconds: startup, + operationNanoseconds: operation, + subscriptionNanoseconds: subscription, + shutdownNanoseconds: shutdown ) } @@ -342,37 +342,37 @@ final class RadrootsRuntimeClientTests: XCTestCase { private func makeConfiguration(generation: String) -> RadrootsRuntimeLaunchConfiguration { RadrootsRuntimeLaunchConfiguration( - applicationSupportDirectory: "/tmp/radroots-runtime-tests-\(generation)", - publicKeyHex: String(repeating: generation, count: 32), - sourceGenerationHex: String(repeating: generation, count: 32), - sourceGenerationCreatedAtUnixMilliseconds: 1, - protectedData: .available, - networkProfile: .simulator, - writableRelays: ["ws://127.0.0.1:7447"], - blossom: RadrootsBlossomEndpointConfiguration( - hostKind: .simulator, - endpointAuthority: .loopbackDevelopment, - primaryOrigin: "http://127.0.0.1:3000", - fallbackOrigins: [] - ), - app: RadrootsRuntimeAppMetadata( - bundleIdentifier: "org.radroots.tests", - version: "0.1.0-alpha", - buildNumber: "1", - buildSHA: nil - ), - signerGeneration: generation, - signer: TestRuntimeSigner(), - adoptBootstrapSettings: false + applicationSupportDirectory: "/tmp/radroots-runtime-tests-\(generation)", + publicKeyHex: String(repeating: generation, count: 32), + sourceGenerationHex: String(repeating: generation, count: 32), + sourceGenerationCreatedAtUnixMilliseconds: 1, + protectedData: .available, + networkProfile: .simulator, + writableRelays: ["ws://127.0.0.1:7447"], + blossom: RadrootsBlossomEndpointConfiguration( + hostKind: .simulator, + endpointAuthority: .loopbackDevelopment, + primaryOrigin: "http://127.0.0.1:3000", + fallbackOrigins: [] + ), + app: RadrootsRuntimeAppMetadata( + bundleIdentifier: "org.radroots.tests", + version: "0.1.0-alpha", + buildNumber: "1", + buildSHA: nil + ), + signerGeneration: generation, + signer: TestRuntimeSigner(), + adoptBootstrapSettings: false ) } private func change(generation: UInt64) -> RadrootsRuntimeChange { RadrootsRuntimeChange( - schemaVersion: 1, - generation: generation, - kind: .today, - entityID: "card-\(generation)" + schemaVersion: 1, + generation: generation, + kind: .today, + entityID: "card-\(generation)" ) } } @@ -400,11 +400,11 @@ private actor RuntimeHarness { private var receivers: [UUID: @Sendable (RadrootsRuntimeChange) async -> Void] = [:] init( - startDelayNanoseconds: UInt64 = 0, - snapshotDelayNanoseconds: UInt64 = 0, - shutdownDelayNanoseconds: UInt64 = 10_000_000, - subscriptionFailures: Int = 0, - shutdownFailure: RadrootsRuntimeFailure? = nil + startDelayNanoseconds: UInt64 = 0, + snapshotDelayNanoseconds: UInt64 = 0, + shutdownDelayNanoseconds: UInt64 = 10_000_000, + subscriptionFailures: Int = 0, + shutdownFailure: RadrootsRuntimeFailure? = nil ) { self.startDelayNanoseconds = startDelayNanoseconds self.snapshotDelayNanoseconds = snapshotDelayNanoseconds @@ -423,15 +423,15 @@ private actor RuntimeHarness { }.value } let backend = TestRuntimeBackend( - harness: self, - publicKeyHex: configuration.publicKeyHex, - snapshotDelayNanoseconds: snapshotDelayNanoseconds, - shutdownDelayNanoseconds: shutdownDelayNanoseconds, - shutdownFailure: shutdownFailure + harness: self, + publicKeyHex: configuration.publicKeyHex, + snapshotDelayNanoseconds: snapshotDelayNanoseconds, + shutdownDelayNanoseconds: shutdownDelayNanoseconds, + shutdownFailure: shutdownFailure ) return await RadrootsRuntimeBackendStart( - backend: backend, - snapshot: backend.snapshotValue() + backend: backend, + snapshot: backend.snapshotValue() ) } @@ -442,9 +442,9 @@ private actor RuntimeHarness { if subscriptionFailures > 0 { subscriptionFailures -= 1 throw RadrootsRuntimeFailure.local( - operation: "test.subscribe", - code: "test.subscribe_retryable", - safeMessage: "The test subscription is temporarily unavailable." + operation: "test.subscribe", + code: "test.subscribe_retryable", + safeMessage: "The test subscription is temporarily unavailable." ) } let id = UUID() @@ -494,27 +494,27 @@ private actor TestRuntimeBackend: RadrootsRuntimeBackend { private var closed = false init( - harness: RuntimeHarness, - publicKeyHex: String, - snapshotDelayNanoseconds: UInt64, - shutdownDelayNanoseconds: UInt64, - shutdownFailure: RadrootsRuntimeFailure? + harness: RuntimeHarness, + publicKeyHex: String, + snapshotDelayNanoseconds: UInt64, + shutdownDelayNanoseconds: UInt64, + shutdownFailure: RadrootsRuntimeFailure? ) { self.harness = harness self.snapshotDelayNanoseconds = snapshotDelayNanoseconds self.shutdownDelayNanoseconds = shutdownDelayNanoseconds self.shutdownFailure = shutdownFailure snapshotValueStored = RadrootsRuntimeSnapshot( - identity: RadrootsRuntimeIdentity( - publicKeyHex: publicKeyHex, - hostSignerConfigured: true - ), - relay: nil, - blossomConfiguration: nil, - blossomEvidence: nil, - crateName: "radroots_mobile_ffi", - crateVersion: "0.1.0-alpha", - isClosed: false + identity: RadrootsRuntimeIdentity( + publicKeyHex: publicKeyHex, + hostSignerConfigured: true + ), + relay: nil, + blossomConfiguration: nil, + blossomEvidence: nil, + crateName: "radroots_mobile_ffi", + crateVersion: "0.1.0-alpha", + isClosed: false ) } @@ -530,9 +530,9 @@ private actor TestRuntimeBackend: RadrootsRuntimeBackend { } if closed { throw RadrootsRuntimeFailure.local( - operation: "test.snapshot", - code: "test.closed", - safeMessage: "The test runtime is closed." + operation: "test.snapshot", + code: "test.closed", + safeMessage: "The test runtime is closed." ) } return snapshotValueStored @@ -540,27 +540,27 @@ private actor TestRuntimeBackend: RadrootsRuntimeBackend { func todayPage(request _: RadrootsTodayPageRequest) throws -> RadrootsTodayPage { throw RadrootsRuntimeFailure.local( - operation: "test.today.page", - code: "test.unsupported", - safeMessage: "Today is not configured for this lifecycle test." + operation: "test.today.page", + code: "test.unsupported", + safeMessage: "Today is not configured for this lifecycle test." ) } func refreshToday( - context _: RadrootsLocalNetwork, - nowUnixSeconds _: UInt64, - update _: RadrootsTodayProjectionUpdate + context _: RadrootsLocalNetwork, + nowUnixSeconds _: UInt64, + update _: RadrootsTodayProjectionUpdate ) throws -> RadrootsTodayRefreshReceipt { throw RadrootsRuntimeFailure.local( - operation: "test.today.refresh", - code: "test.unsupported", - safeMessage: "Today is not configured for this lifecycle test." + operation: "test.today.refresh", + code: "test.unsupported", + safeMessage: "Today is not configured for this lifecycle test." ) } func subscribe( - bufferCapacity _: Int, - receive: @escaping @Sendable (RadrootsRuntimeChange) async -> Void + bufferCapacity _: Int, + receive: @escaping @Sendable (RadrootsRuntimeChange) async -> Void ) async throws -> any RadrootsRuntimeSubscriptionToken { let id = try await harness.addReceiver(receive) return TestSubscriptionToken(harness: harness, id: id) diff --git a/RadrootsTests/RadrootsStateMigrationTests.swift b/RadrootsTests/RadrootsStateMigrationTests.swift @@ -6,64 +6,64 @@ import XCTest final class RadrootsStateMigrationTests: XCTestCase { func testRelayValidationMatchesRuntimeProfiles() throws { XCTAssertEqual( - try RadrootsNetworkValidator.relays( - ["wss://radroots.org", "WSS://WRITE.EXAMPLE:443/"], - profile: .publicNetwork - ), - ["wss://write.example"] + try RadrootsNetworkValidator.relays( + ["wss://radroots.org", "WSS://WRITE.EXAMPLE:443/"], + profile: .publicNetwork + ), + ["wss://write.example"] ) XCTAssertEqual( - try RadrootsNetworkValidator.relays( - ["ws://127.0.0.1:7447"], - profile: .simulator - ), - ["ws://127.0.0.1:7447"] + try RadrootsNetworkValidator.relays( + ["ws://127.0.0.1:7447"], + profile: .simulator + ), + ["ws://127.0.0.1:7447"] ) for accepted in [ - "ws://10.0.0.5:7447", - "ws://172.16.0.5:7447", - "ws://172.31.255.254:7447", - "ws://192.168.0.5:7447", - "ws://[fc00::5]:7447", - "ws://[fd00::5]:7447", - "wss://10.0.0.5:7447", + "ws://10.0.0.5:7447", + "ws://172.16.0.5:7447", + "ws://172.31.255.254:7447", + "ws://192.168.0.5:7447", + "ws://[fc00::5]:7447", + "ws://[fd00::5]:7447", + "wss://10.0.0.5:7447", ] { XCTAssertNoThrow( - try RadrootsNetworkValidator.relays([accepted], profile: .device), - "Expected device policy to admit \(accepted)" + try RadrootsNetworkValidator.relays([accepted], profile: .device), + "Expected device policy to admit \(accepted)" ) } for denied in [ - "ws://8.8.8.8:7447", - "ws://device.example:7447", - "wss://device.example:7447", - "ws://0.0.0.0:7447", - "ws://127.0.0.1:7447", - "ws://169.254.1.1:7447", - "ws://172.32.0.5:7447", - "ws://100.64.0.5:7447", - "ws://224.0.0.1:7447", - "ws://[::]:7447", - "ws://[::1]:7447", - "ws://[fe80::1]:7447", - "ws://[ff02::1]:7447", - "ws://[2001:4860:4860::8888]:7447", + "ws://8.8.8.8:7447", + "ws://device.example:7447", + "wss://device.example:7447", + "ws://0.0.0.0:7447", + "ws://127.0.0.1:7447", + "ws://169.254.1.1:7447", + "ws://172.32.0.5:7447", + "ws://100.64.0.5:7447", + "ws://224.0.0.1:7447", + "ws://[::]:7447", + "ws://[::1]:7447", + "ws://[fe80::1]:7447", + "ws://[ff02::1]:7447", + "ws://[2001:4860:4860::8888]:7447", ] { XCTAssertThrowsError( - try RadrootsNetworkValidator.relays([denied], profile: .device), - "Expected device policy to deny \(denied)" + try RadrootsNetworkValidator.relays([denied], profile: .device), + "Expected device policy to deny \(denied)" ) } for denied in [ - "ws://public.example", - "wss://localhost", - "wss://10.0.0.1", - "wss://user@example.com", - "wss://relay.example?token=value", + "ws://public.example", + "wss://localhost", + "wss://10.0.0.1", + "wss://user@example.com", + "wss://relay.example?token=value", ] { XCTAssertThrowsError( - try RadrootsNetworkValidator.relays([denied], profile: .publicNetwork), - "Expected public policy to deny \(denied)" + try RadrootsNetworkValidator.relays([denied], profile: .publicNetwork), + "Expected public policy to deny \(denied)" ) } } @@ -73,21 +73,21 @@ final class RadrootsStateMigrationTests: XCTestCase { defer { fixture.remove() } let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots) try fileAccess.write( - .inline( + .inline( Data( """ {"format":"radroots_field_ios_relay_settings_v1","relays":["ws://127.0.0.1:7447"]} """.utf8 ) - ), - to: RadrootsFileReference( - scope: .data, - relativePath: "settings/relay_settings.json" - ) + ), + to: RadrootsFileReference( + scope: .data, + relativePath: "settings/relay_settings.json" + ) ) let store = RadrootsConfigurationStore( - bootstrap: fixture.bootstrap, - roots: fixture.roots + bootstrap: fixture.bootstrap, + roots: fixture.roots ) let first = try await store.load() let second = try await store.load() @@ -95,11 +95,11 @@ final class RadrootsStateMigrationTests: XCTestCase { XCTAssertEqual(first.writableRelays, ["ws://127.0.0.1:7447"]) try fileAccess.write( - .inline(Data("not-json".utf8)), - to: RadrootsFileReference( - scope: .data, - relativePath: "settings/radroots_configuration_v3.json" - ) + .inline(Data("not-json".utf8)), + to: RadrootsFileReference( + scope: .data, + relativePath: "settings/radroots_configuration_v3.json" + ) ) do { _ = try await store.load() @@ -114,17 +114,17 @@ final class RadrootsStateMigrationTests: XCTestCase { defer { fixture.remove() } let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots) try fileAccess.write( - .inline( + .inline( Data( """ {"format":"radroots_ios_configuration_v2","profile":"simulator","writableRelays":["ws://127.0.0.1:7447"],"blossomOrigins":["http://127.0.0.1:3000","http://localhost:3001"]} """.utf8 ) - ), - to: RadrootsFileReference( - scope: .data, - relativePath: "settings/radroots_configuration_v2.json" - ) + ), + to: RadrootsFileReference( + scope: .data, + relativePath: "settings/radroots_configuration_v2.json" + ) ) let store = RadrootsConfigurationStore(bootstrap: fixture.bootstrap, roots: fixture.roots) @@ -133,13 +133,13 @@ final class RadrootsStateMigrationTests: XCTestCase { XCTAssertEqual(first, second) XCTAssertEqual( - first.blossom, - RadrootsBlossomEndpointConfiguration( - hostKind: .simulator, - endpointAuthority: .loopbackDevelopment, - primaryOrigin: "http://127.0.0.1:3000", - fallbackOrigins: ["http://localhost:3001"] - ) + first.blossom, + RadrootsBlossomEndpointConfiguration( + hostKind: .simulator, + endpointAuthority: .loopbackDevelopment, + primaryOrigin: "http://127.0.0.1:3000", + fallbackOrigins: ["http://localhost:3001"] + ) ) let persisted = try configurationObject(fileAccess) XCTAssertEqual(persisted["format"] as? String, "radroots_ios_configuration_v3") @@ -151,12 +151,12 @@ final class RadrootsStateMigrationTests: XCTestCase { let fingerprint = String(repeating: "a", count: 64) try await store.confirmCanonicalBlossomConfiguration( - canonicalBlossomConfiguration( - primaryOrigin: "http://127.0.0.1:3000", - fallbackOrigins: ["http://localhost:3001"], - fingerprint: fingerprint - ), - expectedGeneration: first.generation + canonicalBlossomConfiguration( + primaryOrigin: "http://127.0.0.1:3000", + fallbackOrigins: ["http://localhost:3001"], + fingerprint: fingerprint + ), + expectedGeneration: first.generation ) let confirmed = try configurationObject(fileAccess) XCTAssertEqual(confirmed["canonicalBlossomConfigFingerprint"] as? String, fingerprint) @@ -168,25 +168,25 @@ final class RadrootsStateMigrationTests: XCTestCase { defer { fixture.remove() } let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots) _ = try await RadrootsConfigurationStore( - bootstrap: fixture.bootstrap, - roots: fixture.roots + bootstrap: fixture.bootstrap, + roots: fixture.roots ).load() let originalFingerprint = String(repeating: "b", count: 64) let originalStore = RadrootsConfigurationStore( - bootstrap: fixture.bootstrap, - roots: fixture.roots + bootstrap: fixture.bootstrap, + roots: fixture.roots ) try await originalStore.confirmCanonicalBlossomConfiguration( - canonicalBlossomConfiguration(fingerprint: originalFingerprint), - expectedGeneration: 1 + canonicalBlossomConfiguration(fingerprint: originalFingerprint), + expectedGeneration: 1 ) let production = RadrootsConfigurationBootstrap( - runtimeMode: "production", - relayURLs: ["wss://write.example"], - blossomOrigins: ["https://blossom.example"], - keychainServicePrefix: fixture.bootstrap.keychainServicePrefix, - bundleIdentifier: fixture.bootstrap.bundleIdentifier, - appMetadata: fixture.bootstrap.appMetadata + runtimeMode: "production", + relayURLs: ["wss://write.example"], + blossomOrigins: ["https://blossom.example"], + keychainServicePrefix: fixture.bootstrap.keychainServicePrefix, + bundleIdentifier: fixture.bootstrap.bundleIdentifier, + appMetadata: fixture.bootstrap.appMetadata ) let store = RadrootsConfigurationStore(bootstrap: production, roots: fixture.roots) @@ -198,13 +198,13 @@ final class RadrootsStateMigrationTests: XCTestCase { XCTAssertEqual(recovered.previousBlossomConfigFingerprint, originalFingerprint) XCTAssertEqual(recovered.writableRelays, ["wss://write.example"]) XCTAssertEqual( - recovered.blossom, - RadrootsBlossomEndpointConfiguration( - hostKind: .physicalDevice, - endpointAuthority: .publicWebPKI, - primaryOrigin: "https://blossom.example", - fallbackOrigins: [] - ) + recovered.blossom, + RadrootsBlossomEndpointConfiguration( + hostKind: .physicalDevice, + endpointAuthority: .publicWebPKI, + primaryOrigin: "https://blossom.example", + fallbackOrigins: [] + ) ) let repeated = try await store.load() XCTAssertEqual(repeated, recovered) @@ -214,8 +214,8 @@ final class RadrootsStateMigrationTests: XCTestCase { XCTAssertEqual(persisted["activationState"] as? String, "reconfiguration_required") XCTAssertNil(persisted["canonicalBlossomConfigFingerprint"]) XCTAssertEqual( - persisted["previousBlossomConfigFingerprint"] as? String, - originalFingerprint + persisted["previousBlossomConfigFingerprint"] as? String, + originalFingerprint ) } @@ -223,26 +223,26 @@ final class RadrootsStateMigrationTests: XCTestCase { let fixture = try StateFixture() defer { fixture.remove() } let original = RadrootsConfigurationStore( - bootstrap: fixture.bootstrap, - roots: fixture.roots + bootstrap: fixture.bootstrap, + roots: fixture.roots ) let first = try await original.load() let originalFingerprint = String(repeating: "c", count: 64) try await original.confirmCanonicalBlossomConfiguration( - canonicalBlossomConfiguration(fingerprint: originalFingerprint), - expectedGeneration: first.generation + canonicalBlossomConfiguration(fingerprint: originalFingerprint), + expectedGeneration: first.generation ) let changedBootstrap = RadrootsConfigurationBootstrap( - runtimeMode: fixture.bootstrap.runtimeMode, - relayURLs: ["ws://127.0.0.1:7448"], - blossomOrigins: ["http://127.0.0.1:3001"], - keychainServicePrefix: fixture.bootstrap.keychainServicePrefix, - bundleIdentifier: fixture.bootstrap.bundleIdentifier, - appMetadata: fixture.bootstrap.appMetadata + runtimeMode: fixture.bootstrap.runtimeMode, + relayURLs: ["ws://127.0.0.1:7448"], + blossomOrigins: ["http://127.0.0.1:3001"], + keychainServicePrefix: fixture.bootstrap.keychainServicePrefix, + bundleIdentifier: fixture.bootstrap.bundleIdentifier, + appMetadata: fixture.bootstrap.appMetadata ) let changed = RadrootsConfigurationStore( - bootstrap: changedBootstrap, - roots: fixture.roots + bootstrap: changedBootstrap, + roots: fixture.roots ) let pending = try await changed.load() @@ -255,11 +255,11 @@ final class RadrootsStateMigrationTests: XCTestCase { let replacementFingerprint = String(repeating: "d", count: 64) try await changed.confirmCanonicalBlossomConfiguration( - canonicalBlossomConfiguration( - primaryOrigin: "http://127.0.0.1:3001", - fingerprint: replacementFingerprint - ), - expectedGeneration: pending.generation + canonicalBlossomConfiguration( + primaryOrigin: "http://127.0.0.1:3001", + fingerprint: replacementFingerprint + ), + expectedGeneration: pending.generation ) let active = try await changed.load() XCTAssertEqual(active.activationState, .current) @@ -269,8 +269,8 @@ final class RadrootsStateMigrationTests: XCTestCase { RadrootsAppleFileAccess(roots: fixture.roots) ) XCTAssertEqual( - persisted["canonicalBlossomConfigFingerprint"] as? String, - replacementFingerprint + persisted["canonicalBlossomConfigFingerprint"] as? String, + replacementFingerprint ) } @@ -278,25 +278,25 @@ final class RadrootsStateMigrationTests: XCTestCase { let fixture = try StateFixture() defer { fixture.remove() } let original = RadrootsConfigurationStore( - bootstrap: fixture.bootstrap, - roots: fixture.roots + bootstrap: fixture.bootstrap, + roots: fixture.roots ) let first = try await original.load() try await original.confirmCanonicalBlossomConfiguration( - canonicalBlossomConfiguration(fingerprint: String(repeating: "f", count: 64)), - expectedGeneration: first.generation + canonicalBlossomConfiguration(fingerprint: String(repeating: "f", count: 64)), + expectedGeneration: first.generation ) let changedBootstrap = RadrootsConfigurationBootstrap( - runtimeMode: fixture.bootstrap.runtimeMode, - relayURLs: ["ws://127.0.0.1:7448"], - blossomOrigins: ["http://127.0.0.1:3001"], - keychainServicePrefix: fixture.bootstrap.keychainServicePrefix, - bundleIdentifier: fixture.bootstrap.bundleIdentifier, - appMetadata: fixture.bootstrap.appMetadata + runtimeMode: fixture.bootstrap.runtimeMode, + relayURLs: ["ws://127.0.0.1:7448"], + blossomOrigins: ["http://127.0.0.1:3001"], + keychainServicePrefix: fixture.bootstrap.keychainServicePrefix, + bundleIdentifier: fixture.bootstrap.bundleIdentifier, + appMetadata: fixture.bootstrap.appMetadata ) let changed = RadrootsConfigurationStore( - bootstrap: changedBootstrap, - roots: fixture.roots + bootstrap: changedBootstrap, + roots: fixture.roots ) let pending = try await changed.load() @@ -319,26 +319,26 @@ final class RadrootsStateMigrationTests: XCTestCase { let fixture = try StateFixture() defer { fixture.remove() } let bootstrap = RadrootsConfigurationBootstrap( - runtimeMode: "production", - relayURLs: [], - blossomOrigins: ["https://media.example"], - keychainServicePrefix: fixture.bootstrap.keychainServicePrefix, - bundleIdentifier: fixture.bootstrap.bundleIdentifier, - appMetadata: fixture.bootstrap.appMetadata + runtimeMode: "production", + relayURLs: [], + blossomOrigins: ["https://media.example"], + keychainServicePrefix: fixture.bootstrap.keychainServicePrefix, + bundleIdentifier: fixture.bootstrap.bundleIdentifier, + appMetadata: fixture.bootstrap.appMetadata ) let store = RadrootsConfigurationStore(bootstrap: bootstrap, roots: fixture.roots) let selected = try await store.load() try await store.confirmCanonicalBlossomConfiguration( - RadrootsBlossomConfigurationStatus( - schemaVersion: 1, - hostKind: "physical_device", - endpointAuthority: "public_webpki", - primaryOrigin: "https://media.example", - fallbackOrigins: [], - configFingerprint: String(repeating: "e", count: 64) - ), - expectedGeneration: selected.generation + RadrootsBlossomConfigurationStatus( + schemaVersion: 1, + hostKind: "physical_device", + endpointAuthority: "public_webpki", + primaryOrigin: "https://media.example", + fallbackOrigins: [], + configFingerprint: String(repeating: "e", count: 64) + ), + expectedGeneration: selected.generation ) let persisted = try configurationObject( @@ -353,30 +353,30 @@ final class RadrootsStateMigrationTests: XCTestCase { defer { fixture.remove() } let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots) try fileAccess.write( - .inline( + .inline( Data( """ {"format":"radroots_ios_configuration_v3","profile":"simulator","writableRelays":["ws://127.0.0.1:7447"],"blossom":{"hostKind":"physical_device","endpointAuthority":"public_web_pki","primaryOrigin":"https://wrong.example","fallbackOrigins":[]}} """.utf8 ) - ), - to: RadrootsFileReference( - scope: .data, - relativePath: "settings/radroots_configuration_v3.json" - ) + ), + to: RadrootsFileReference( + scope: .data, + relativePath: "settings/radroots_configuration_v3.json" + ) ) let store = RadrootsConfigurationStore(bootstrap: fixture.bootstrap, roots: fixture.roots) let recovered = try await store.load() XCTAssertEqual( - recovered.blossom, - RadrootsBlossomEndpointConfiguration( - hostKind: .simulator, - endpointAuthority: .loopbackDevelopment, - primaryOrigin: "http://127.0.0.1:3000", - fallbackOrigins: [] - ) + recovered.blossom, + RadrootsBlossomEndpointConfiguration( + hostKind: .simulator, + endpointAuthority: .loopbackDevelopment, + primaryOrigin: "http://127.0.0.1:3000", + fallbackOrigins: [] + ) ) } @@ -384,11 +384,11 @@ final class RadrootsStateMigrationTests: XCTestCase { _ fileAccess: RadrootsAppleFileAccess ) throws -> [String: Any] { let source = try fileAccess.read( - RadrootsFileReference( - scope: .data, - relativePath: "settings/radroots_configuration_v3.json" - ), - mode: .inline + RadrootsFileReference( + scope: .data, + relativePath: "settings/radroots_configuration_v3.json" + ), + mode: .inline ) guard case let .inline(data) = source, let object = try JSONSerialization.jsonObject(with: data) as? [String: Any] @@ -399,17 +399,17 @@ final class RadrootsStateMigrationTests: XCTestCase { } private func canonicalBlossomConfiguration( - primaryOrigin: String = "http://127.0.0.1:3000", - fallbackOrigins: [String] = [], - fingerprint: String + primaryOrigin: String = "http://127.0.0.1:3000", + fallbackOrigins: [String] = [], + fingerprint: String ) -> RadrootsBlossomConfigurationStatus { RadrootsBlossomConfigurationStatus( - schemaVersion: 1, - hostKind: "simulator", - endpointAuthority: "loopback_development", - primaryOrigin: primaryOrigin, - fallbackOrigins: fallbackOrigins, - configFingerprint: fingerprint + schemaVersion: 1, + hostKind: "simulator", + endpointAuthority: "loopback_development", + primaryOrigin: primaryOrigin, + fallbackOrigins: fallbackOrigins, + configFingerprint: fingerprint ) } @@ -417,25 +417,25 @@ final class RadrootsStateMigrationTests: XCTestCase { let fixture = try StateFixture() defer { fixture.remove() } let firstStore = RadrootsConfigurationStore( - bootstrap: fixture.bootstrap, - roots: fixture.roots + bootstrap: fixture.bootstrap, + roots: fixture.roots ) let first = try await firstStore.sourceGeneration() let secondStore = RadrootsConfigurationStore( - bootstrap: fixture.bootstrap, - roots: fixture.roots + bootstrap: fixture.bootstrap, + roots: fixture.roots ) let second = try await secondStore.sourceGeneration() XCTAssertEqual(first, second) let key = String(repeating: "ab", count: 32) XCTAssertEqual( - RadrootsStableVisualIdentity(publicKeyHex: key), - RadrootsStableVisualIdentity(publicKeyHex: key) + RadrootsStableVisualIdentity(publicKeyHex: key), + RadrootsStableVisualIdentity(publicKeyHex: key) ) XCTAssertNotEqual( - RadrootsStableVisualIdentity(publicKeyHex: key).digestHex, - RadrootsStableVisualIdentity(publicKeyHex: String(repeating: "cd", count: 32)).digestHex + RadrootsStableVisualIdentity(publicKeyHex: key).digestHex, + RadrootsStableVisualIdentity(publicKeyHex: String(repeating: "cd", count: 32)).digestHex ) } @@ -448,28 +448,28 @@ final class RadrootsStateMigrationTests: XCTestCase { UserDefaults(suiteName: servicePrefix)?.removePersistentDomain(forName: servicePrefix) } let legacyKey = RadrootsSecureStoreKey( - namespace: "nostr_identity", - name: "selected_secret_hex" + namespace: "nostr_identity", + name: "selected_secret_hex" ) try secureStore.put( - Data(String(repeating: "01", count: 32).utf8), - for: legacyKey, - policy: .secureLocalSecret + Data(String(repeating: "01", count: 32).utf8), + for: legacyKey, + policy: .secureLocalSecret ) let custody = try RadrootsIdentityCustody( - configuration: RadrootsIdentityCustodyConfiguration( - namespace: "radroots_identity_v1", - secretPolicy: .secureLocalSecret - ), - secureStore: secureStore, - metadataStore: metadataStore, - userPresence: AllowingUserPresence() + configuration: RadrootsIdentityCustodyConfiguration( + namespace: "radroots_identity_v1", + secretPolicy: .secureLocalSecret + ), + secureStore: secureStore, + metadataStore: metadataStore, + userPresence: AllowingUserPresence() ) let store = RadrootsIdentityStore( - custody: custody, - secureStore: secureStore, - servicePrefix: servicePrefix, - userDefaults: defaults + custody: custody, + secureStore: secureStore, + servicePrefix: servicePrefix, + userDefaults: defaults ) let first = try await store.loadAndMigrate() XCTAssertEqual(first.state, .recoveryRequired) @@ -489,23 +489,23 @@ final class RadrootsStateMigrationTests: XCTestCase { UserDefaults(suiteName: servicePrefix)?.removePersistentDomain(forName: servicePrefix) } defaults.set( - Data("not-json".utf8), - forKey: "field_ios.identity.public_metadata.\(servicePrefix)" + Data("not-json".utf8), + forKey: "field_ios.identity.public_metadata.\(servicePrefix)" ) let custody = try RadrootsIdentityCustody( - configuration: RadrootsIdentityCustodyConfiguration( - namespace: "radroots_identity_v1", - secretPolicy: .secureLocalSecret - ), - secureStore: secureStore, - metadataStore: InMemoryIdentityMetadataStore(), - userPresence: AllowingUserPresence() + configuration: RadrootsIdentityCustodyConfiguration( + namespace: "radroots_identity_v1", + secretPolicy: .secureLocalSecret + ), + secureStore: secureStore, + metadataStore: InMemoryIdentityMetadataStore(), + userPresence: AllowingUserPresence() ) let store = RadrootsIdentityStore( - custody: custody, - secureStore: secureStore, - servicePrefix: servicePrefix, - userDefaults: defaults + custody: custody, + secureStore: secureStore, + servicePrefix: servicePrefix, + userDefaults: defaults ) do { _ = try await store.loadAndMigrate() @@ -518,32 +518,32 @@ final class RadrootsStateMigrationTests: XCTestCase { func testRuntimeSignerUsesCanonicalOperationIDInsteadOfSignerRequestDigest() async throws { let secureStore = InMemorySecureStore() let custody = try RadrootsIdentityCustody( - configuration: RadrootsIdentityCustodyConfiguration( - namespace: "radroots_identity_v1", - secretPolicy: .secureLocalSecret - ), - secureStore: secureStore, - metadataStore: InMemoryIdentityMetadataStore(), - userPresence: AllowingUserPresence() + configuration: RadrootsIdentityCustodyConfiguration( + namespace: "radroots_identity_v1", + secretPolicy: .secureLocalSecret + ), + secureStore: secureStore, + metadataStore: InMemoryIdentityMetadataStore(), + userPresence: AllowingUserPresence() ) let servicePrefix = "org.radroots.tests.signer.\(UUID().uuidString.lowercased())" let store = RadrootsIdentityStore( - custody: custody, - secureStore: secureStore, - servicePrefix: servicePrefix + custody: custody, + secureStore: secureStore, + servicePrefix: servicePrefix ) let identity = try await store.create() let signer = try await store.signer(for: identity) let operationID = UUID().uuidString.lowercased() - let outcome = await signer.sign( + let outcome = try await signer.sign( RadrootsRuntimeSigningRequest( - operationID: operationID, - signerRequestID: String(repeating: "ab", count: 32), - publicKeyHex: try XCTUnwrap(identity.publicKeyHex), - purpose: .blossomUpload, - deadlineUnixMilliseconds: UInt64(Date().timeIntervalSince1970 * 1000) + 60_000, - digest: Data(repeating: 0xcd, count: 32) + operationID: operationID, + signerRequestID: String(repeating: "ab", count: 32), + publicKeyHex: XCTUnwrap(identity.publicKeyHex), + purpose: .blossomUpload, + deadlineUnixMilliseconds: UInt64(Date().timeIntervalSince1970 * 1000) + 60000, + digest: Data(repeating: 0xCD, count: 32) ) ) @@ -564,23 +564,23 @@ private struct StateFixture { .appendingPathComponent("radroots-state-tests-\(UUID().uuidString.lowercased())") try FileManager.default.createDirectory(at: root, withIntermediateDirectories: false) roots = try RadrootsAppleFileRoots( - appIdentifier: "org.radroots.tests", - dataRoot: root.appendingPathComponent("data"), - cacheRoot: root.appendingPathComponent("cache"), - temporaryRoot: root.appendingPathComponent("tmp") + appIdentifier: "org.radroots.tests", + dataRoot: root.appendingPathComponent("data"), + cacheRoot: root.appendingPathComponent("cache"), + temporaryRoot: root.appendingPathComponent("tmp") ) bootstrap = RadrootsConfigurationBootstrap( - runtimeMode: "localhost-dev", - relayURLs: ["ws://127.0.0.1:8080"], - blossomOrigins: ["http://127.0.0.1:3000"], - keychainServicePrefix: "org.radroots.tests", + runtimeMode: "localhost-dev", + relayURLs: ["ws://127.0.0.1:8080"], + blossomOrigins: ["http://127.0.0.1:3000"], + keychainServicePrefix: "org.radroots.tests", + bundleIdentifier: "org.radroots.tests", + appMetadata: RadrootsRuntimeAppMetadata( bundleIdentifier: "org.radroots.tests", - appMetadata: RadrootsRuntimeAppMetadata( - bundleIdentifier: "org.radroots.tests", - version: "0.1.0-alpha", - buildNumber: "1", - buildSHA: nil - ) + version: "0.1.0-alpha", + buildNumber: "1", + buildSHA: nil + ) ) } @@ -594,9 +594,9 @@ private final class InMemorySecureStore: RadrootsSecureStore, @unchecked Sendabl private var values: [RadrootsSecureStoreKey: Data] = [:] func put( - _ value: Data, - for key: RadrootsSecureStoreKey, - policy _: RadrootsSecretAccessPolicy + _ value: Data, + for key: RadrootsSecureStoreKey, + policy _: RadrootsSecretAccessPolicy ) throws { lock.withLock { values[key] = value } } @@ -638,10 +638,10 @@ private final class InMemoryIdentityMetadataStore: RadrootsIdentityMetadataStore private struct AllowingUserPresence: RadrootsUserPresence { func currentStatus() async throws -> RadrootsUserPresenceStatus { RadrootsUserPresenceStatus( - support: .deviceCredential, - biometryKind: .none, - canEvaluateDeviceCredential: true, - canEvaluateBiometrics: false + support: .deviceCredential, + biometryKind: .none, + canEvaluateDeviceCredential: true, + canEvaluateBiometrics: false ) } diff --git a/RadrootsTests/RadrootsSupportingStoreTests.swift b/RadrootsTests/RadrootsSupportingStoreTests.swift @@ -81,8 +81,8 @@ final class RadrootsSupportingStoreTests: XCTestCase { XCTAssertEqual(store.settings?.identity.identities.count, 1) store.networkEnvironment = .physicalDevice store.relays = [ - RadrootsRelayPreference(url: "wss://radroots.org/", access: .readWrite), - RadrootsRelayPreference(url: "wss://read.example/", access: .readOnly), + RadrootsRelayPreference(url: "wss://radroots.org/", access: .readWrite), + RadrootsRelayPreference(url: "wss://read.example/", access: .readOnly), ] store.blossomPrimaryOrigin = "https://blossom.radroots.org" store.allowCellularUploads = false @@ -147,40 +147,40 @@ final class RadrootsSupportingStoreTests: XCTestCase { private static func configuration() -> RadrootsRuntimeLaunchConfiguration { RadrootsRuntimeLaunchConfiguration( - applicationSupportDirectory: "/tmp/radroots-supporting-tests", - publicKeyHex: String(repeating: "a", count: 64), - sourceGenerationHex: String(repeating: "c", count: 64), - sourceGenerationCreatedAtUnixMilliseconds: 1, - protectedData: .available, - networkProfile: .simulator, - writableRelays: ["ws://127.0.0.1:7447"], - blossom: RadrootsBlossomEndpointConfiguration( - hostKind: .simulator, - endpointAuthority: .loopbackDevelopment, - primaryOrigin: "http://127.0.0.1:3000", - fallbackOrigins: [] - ), - app: RadrootsRuntimeAppMetadata( - bundleIdentifier: "org.radroots.supporting-tests", - version: "0.1.0-alpha", - buildNumber: "1", - buildSHA: nil - ), - signerGeneration: "supporting-tests", - signer: SupportingSigner(), - adoptBootstrapSettings: false + applicationSupportDirectory: "/tmp/radroots-supporting-tests", + publicKeyHex: String(repeating: "a", count: 64), + sourceGenerationHex: String(repeating: "c", count: 64), + sourceGenerationCreatedAtUnixMilliseconds: 1, + protectedData: .available, + networkProfile: .simulator, + writableRelays: ["ws://127.0.0.1:7447"], + blossom: RadrootsBlossomEndpointConfiguration( + hostKind: .simulator, + endpointAuthority: .loopbackDevelopment, + primaryOrigin: "http://127.0.0.1:3000", + fallbackOrigins: [] + ), + app: RadrootsRuntimeAppMetadata( + bundleIdentifier: "org.radroots.supporting-tests", + version: "0.1.0-alpha", + buildNumber: "1", + buildSHA: nil + ), + signerGeneration: "supporting-tests", + signer: SupportingSigner(), + adoptBootstrapSettings: false ) } private static func context(id: String) -> RadrootsLocalNetwork { RadrootsLocalNetwork( - schemaVersion: 1, - id: id, - label: "Farm", - relayURLs: ["ws://127.0.0.1:7447"], - locality: "Metchosin", - followedAuthors: [], - generation: 1 + schemaVersion: 1, + id: id, + label: "Farm", + relayURLs: ["ws://127.0.0.1:7447"], + locality: "Metchosin", + followedAuthors: [], + generation: 1 ) } } @@ -213,16 +213,16 @@ private actor SupportingBackend: RadrootsRuntimeBackend { func snapshotValue() -> RadrootsRuntimeSnapshot { RadrootsRuntimeSnapshot( - identity: RadrootsRuntimeIdentity( - publicKeyHex: String(repeating: "a", count: 64), - hostSignerConfigured: true - ), - relay: nil, - blossomConfiguration: nil, - blossomEvidence: nil, - crateName: "radroots_mobile_ffi", - crateVersion: "0.1.0-alpha", - isClosed: closed + identity: RadrootsRuntimeIdentity( + publicKeyHex: String(repeating: "a", count: 64), + hostSignerConfigured: true + ), + relay: nil, + blossomConfiguration: nil, + blossomEvidence: nil, + crateName: "radroots_mobile_ffi", + crateVersion: "0.1.0-alpha", + isClosed: closed ) } @@ -235,18 +235,18 @@ private actor SupportingBackend: RadrootsRuntimeBackend { } func refreshToday( - context _: RadrootsLocalNetwork, - nowUnixSeconds _: UInt64, - update _: RadrootsTodayProjectionUpdate + context _: RadrootsLocalNetwork, + nowUnixSeconds _: UInt64, + update _: RadrootsTodayProjectionUpdate ) throws -> RadrootsTodayRefreshReceipt { throw unsupported() } func search( - context: RadrootsLocalNetwork, - query: String, - limit _: UInt16, - asOfUnixSeconds _: UInt64 + context: RadrootsLocalNetwork, + query: String, + limit _: UInt16, + asOfUnixSeconds _: UInt64 ) async throws -> [RadrootsSearchResult] { request = SearchRequest(contextID: context.id, query: query) if searchDelayNanoseconds > 0 { @@ -255,20 +255,20 @@ private actor SupportingBackend: RadrootsRuntimeBackend { let card = Self.card() let profile = Self.profile() return [ - RadrootsSearchResult(type: .card, id: "card", card: card, profile: nil), - RadrootsSearchResult(type: .card, id: "card", card: card, profile: nil), - RadrootsSearchResult(type: .profile, id: "profile", card: nil, profile: profile), + RadrootsSearchResult(type: .card, id: "card", card: card, profile: nil), + RadrootsSearchResult(type: .card, id: "card", card: card, profile: nil), + RadrootsSearchResult(type: .profile, id: "profile", card: nil, profile: profile), ] } func me( - context _: RadrootsLocalNetwork, - asOfUnixSeconds _: UInt64 + context _: RadrootsLocalNetwork, + asOfUnixSeconds _: UInt64 ) -> RadrootsMeSnapshot { RadrootsMeSnapshot( - publicKey: String(repeating: "a", count: 64), - profile: Self.profile(), - cards: [Self.card()] + publicKey: String(repeating: "a", count: 64), + profile: Self.profile(), + cards: [Self.card()] ) } @@ -286,24 +286,24 @@ private actor SupportingBackend: RadrootsRuntimeBackend { throw failure(code: "invalid_relay_endpoint") } settings = RadrootsMobileSettings( - revision: settings.revision + 1, - identity: settings.identity, - networkEnvironment: input.networkEnvironment, - relays: input.relays, - blossomAuthority: input.blossomAuthority, - blossomPrimaryOrigin: input.blossomPrimaryOrigin, - blossomFallbackOrigins: input.blossomFallbackOrigins, - allowCellularDownloads: input.allowCellularDownloads, - allowCellularUploads: input.allowCellularUploads, - allowBackgroundTransfers: input.allowBackgroundTransfers, - mediaCacheBytes: input.mediaCacheBytes, - mediaCacheArtifacts: input.mediaCacheArtifacts + revision: settings.revision + 1, + identity: settings.identity, + networkEnvironment: input.networkEnvironment, + relays: input.relays, + blossomAuthority: input.blossomAuthority, + blossomPrimaryOrigin: input.blossomPrimaryOrigin, + blossomFallbackOrigins: input.blossomFallbackOrigins, + allowCellularDownloads: input.allowCellularDownloads, + allowCellularUploads: input.allowCellularUploads, + allowBackgroundTransfers: input.allowBackgroundTransfers, + mediaCacheBytes: input.mediaCacheBytes, + mediaCacheArtifacts: input.mediaCacheArtifacts ) return RadrootsSettingsTransition( - settings: settings, - runtimeRestartRequired: true, - outboxRequeueRequired: true, - mediaCacheInvalidationRequired: true + settings: settings, + runtimeRestartRequired: true, + outboxRequeueRequired: true, + mediaCacheInvalidationRequired: true ) } @@ -314,31 +314,31 @@ private actor SupportingBackend: RadrootsRuntimeBackend { func advanceProfile(operationID _: String) -> RadrootsProfileStatus { profileStatus( - state: .complete, - revision: 2, - settlement: RadrootsOperationSettlement( - artifacts: 0, - signed: 1, - admitted: 1, - pending: 0, - retryable: 0, - indeterminate: 0, - failedTerminal: 0, - cancelled: 0, - deliveryPlans: 1, - deliverySatisfied: 1, - deliveryPending: 0, - deliveryRetryable: 0, - deliveryExhausted: 0, - deliveryFailedTerminal: 0, - deliveryCancelled: 0 - ) + state: .complete, + revision: 2, + settlement: RadrootsOperationSettlement( + artifacts: 0, + signed: 1, + admitted: 1, + pending: 0, + retryable: 0, + indeterminate: 0, + failedTerminal: 0, + cancelled: 0, + deliveryPlans: 1, + deliverySatisfied: 1, + deliveryPending: 0, + deliveryRetryable: 0, + deliveryExhausted: 0, + deliveryFailedTerminal: 0, + deliveryCancelled: 0 + ) ) } func subscribe( - bufferCapacity _: Int, - receive _: @escaping @Sendable (RadrootsRuntimeChange) async -> Void + bufferCapacity _: Int, + receive _: @escaping @Sendable (RadrootsRuntimeChange) async -> Void ) -> any RadrootsRuntimeSubscriptionToken { SupportingSubscriptionToken() } @@ -359,116 +359,116 @@ private actor SupportingBackend: RadrootsRuntimeBackend { private func failure(code: String) -> RadrootsRuntimeFailure { RadrootsRuntimeFailure( - schemaVersion: 1, - code: code, - category: "invalid_argument", - retryable: false, - recoveryActions: [], - operationID: "test.settings", - capabilityID: nil, - safeMessage: "The settings value is invalid." + schemaVersion: 1, + code: code, + category: "invalid_argument", + retryable: false, + recoveryActions: [], + operationID: "test.settings", + capabilityID: nil, + safeMessage: "The settings value is invalid." ) } private func profileStatus( - state: RadrootsOutboxState, - revision: UInt64, - settlement: RadrootsOperationSettlement? + state: RadrootsOutboxState, + revision: UInt64, + settlement: RadrootsOperationSettlement? ) -> RadrootsProfileStatus { RadrootsProfileStatus( - id: "profile-operation", - revision: revision, - authorPublicKey: String(repeating: "a", count: 64), - state: state, - deliveryID: state == .complete ? "delivery" : nil, - createdAtUnixMilliseconds: 1, - updatedAtUnixMilliseconds: revision, - settlement: settlement + id: "profile-operation", + revision: revision, + authorPublicKey: String(repeating: "a", count: 64), + state: state, + deliveryID: state == .complete ? "delivery" : nil, + createdAtUnixMilliseconds: 1, + updatedAtUnixMilliseconds: revision, + settlement: settlement ) } private static func initialSettings() -> RadrootsMobileSettings { RadrootsMobileSettings( - revision: 1, - identity: RadrootsSettingsIdentityState( - identities: [ - RadrootsSettingsIdentity( - id: "identity", - publicKeyHex: String(repeating: "a", count: 64) - ), - ], - activeIdentityID: "identity", - lockState: .unlocked, - pendingImportOperationID: nil - ), - networkEnvironment: .simulator, - relays: [ - RadrootsRelayPreference(url: "ws://127.0.0.1:7447", access: .readWrite), + revision: 1, + identity: RadrootsSettingsIdentityState( + identities: [ + RadrootsSettingsIdentity( + id: "identity", + publicKeyHex: String(repeating: "a", count: 64) + ), ], - blossomAuthority: .loopbackDevelopment, - blossomPrimaryOrigin: "http://127.0.0.1:3000", - blossomFallbackOrigins: [], - allowCellularDownloads: true, - allowCellularUploads: true, - allowBackgroundTransfers: true, - mediaCacheBytes: 256 * 1_048_576, - mediaCacheArtifacts: 1024 + activeIdentityID: "identity", + lockState: .unlocked, + pendingImportOperationID: nil + ), + networkEnvironment: .simulator, + relays: [ + RadrootsRelayPreference(url: "ws://127.0.0.1:7447", access: .readWrite), + ], + blossomAuthority: .loopbackDevelopment, + blossomPrimaryOrigin: "http://127.0.0.1:3000", + blossomFallbackOrigins: [], + allowCellularDownloads: true, + allowCellularUploads: true, + allowBackgroundTransfers: true, + mediaCacheBytes: 256 * 1_048_576, + mediaCacheArtifacts: 1024 ) } private func unsupported() -> RadrootsRuntimeFailure { .local( - operation: "test.supporting", - code: "test.unsupported", - safeMessage: "Unsupported test operation." + operation: "test.supporting", + code: "test.unsupported", + safeMessage: "Unsupported test operation." ) } private static func profile() -> RadrootsProfileSummary { RadrootsProfileSummary( - authorPublicKey: String(repeating: "a", count: 64), - name: "moss", - displayName: "Moss Farm", - about: "Local roots", - picture: nil, - banner: nil, - nip05: "moss@example.com", - website: "https://moss.example", - lightningAddress: "moss@example.com" + authorPublicKey: String(repeating: "a", count: 64), + name: "moss", + displayName: "Moss Farm", + about: "Local roots", + picture: nil, + banner: nil, + nip05: "moss@example.com", + website: "https://moss.example", + lightningAddress: "moss@example.com" ) } private static func card() -> RadrootsTodayCard { RadrootsTodayCard( - id: "card", - type: .foodAvailability, - sourceEventID: String(repeating: "e", count: 64), - sourceAddress: nil, - authorPublicKey: String(repeating: "a", count: 64), - contractID: "radroots.food_availability.v1", - title: "Carrots", - content: "Freshly picked", - authoredAtUnixSeconds: 1_800_000_000, - effectiveAtUnixSeconds: 1_800_000_000, - eventStartUnixSeconds: nil, - eventEndUnixSeconds: nil, - location: "Metchosin", - priceAmount: "3", - priceCurrency: "CAD", - priceUnit: "lb", - quantity: "12", - foodSummary: "Fresh carrots", - foodPublishedAtUnixSeconds: 1_800_000_000, - foodStatus: "active", - contextRank: 1, - inclusionReason: "local", - media: [], - lifecycle: .active, - rankDigest: nil, - authorProfile: profile(), - thread: [], - localOperationID: nil, - localOperationState: nil + id: "card", + type: .foodAvailability, + sourceEventID: String(repeating: "e", count: 64), + sourceAddress: nil, + authorPublicKey: String(repeating: "a", count: 64), + contractID: "radroots.food_availability.v1", + title: "Carrots", + content: "Freshly picked", + authoredAtUnixSeconds: 1_800_000_000, + effectiveAtUnixSeconds: 1_800_000_000, + eventStartUnixSeconds: nil, + eventEndUnixSeconds: nil, + location: "Metchosin", + priceAmount: "3", + priceCurrency: "CAD", + priceUnit: "lb", + quantity: "12", + foodSummary: "Fresh carrots", + foodPublishedAtUnixSeconds: 1_800_000_000, + foodStatus: "active", + contextRank: 1, + inclusionReason: "local", + media: [], + lifecycle: .active, + rankDigest: nil, + authorProfile: profile(), + thread: [], + localOperationID: nil, + localOperationState: nil ) } } diff --git a/RadrootsTests/RadrootsTodayStoreTests.swift b/RadrootsTests/RadrootsTodayStoreTests.swift @@ -9,24 +9,24 @@ final class RadrootsTodayStoreTests: XCTestCase { let second = makeCard(id: "b", type: .ask, authoredAt: 1_800_000_000) let backend = TodayBackend( pages: [ - "one:first": RadrootsTodayPage( - asOfUnixSeconds: 1_800_000_100, - items: [first], - nextCursor: "same-time" - ), - "one:same-time": RadrootsTodayPage( - asOfUnixSeconds: 1_800_000_100, - items: [first, second], - nextCursor: nil - ), + "one:first": RadrootsTodayPage( + asOfUnixSeconds: 1_800_000_100, + items: [first], + nextCursor: "same-time" + ), + "one:same-time": RadrootsTodayPage( + asOfUnixSeconds: 1_800_000_100, + items: [first, second], + nextCursor: nil + ), ] ) let client = try await startedClient(backend: backend) let store = RadrootsTodayStore( - runtimeClient: client, - contexts: [context], - pageSize: 1, - now: { 1_800_000_100 } + runtimeClient: client, + contexts: [context], + pageSize: 1, + now: { 1_800_000_100 } ) await store.reload(refreshProjection: false) @@ -47,25 +47,25 @@ final class RadrootsTodayStoreTests: XCTestCase { let stale = makeCard(id: "stale", type: .update) let current = makeCard(id: "current", type: .photoUpdate) let backend = TodayBackend( - pages: [ - "slow:first": RadrootsTodayPage( - asOfUnixSeconds: 1_800_000_100, - items: [stale], - nextCursor: nil - ), - "current:first": RadrootsTodayPage( - asOfUnixSeconds: 1_800_000_100, - items: [current], - nextCursor: nil - ), - ], - delays: ["slow": 80_000_000] + pages: [ + "slow:first": RadrootsTodayPage( + asOfUnixSeconds: 1_800_000_100, + items: [stale], + nextCursor: nil + ), + "current:first": RadrootsTodayPage( + asOfUnixSeconds: 1_800_000_100, + items: [current], + nextCursor: nil + ), + ], + delays: ["slow": 80_000_000] ) let client = try await startedClient(backend: backend) let store = RadrootsTodayStore( - runtimeClient: client, - contexts: [firstContext, secondContext], - now: { 1_800_000_100 } + runtimeClient: client, + contexts: [firstContext, secondContext], + now: { 1_800_000_100 } ) let staleRequest = Task { await store.reload(refreshProjection: false) } @@ -82,45 +82,45 @@ final class RadrootsTodayStoreTests: XCTestCase { @MainActor func testFiveCardsExposeProfileThreadMediaOverlayAndAccessibleProductFields() { let profile = RadrootsProfileSummary( - authorPublicKey: String(repeating: "a", count: 64), - name: "fieldnotes", - displayName: "Field Notes Farm", - about: nil, - picture: nil, - banner: nil, - nip05: "farm@example.com", - website: nil, - lightningAddress: nil + authorPublicKey: String(repeating: "a", count: 64), + name: "fieldnotes", + displayName: "Field Notes Farm", + about: nil, + picture: nil, + banner: nil, + nip05: "farm@example.com", + website: nil, + lightningAddress: nil ) let failedMedia = RadrootsMediaReference( - referenceFingerprint: String(repeating: "d", count: 64), - url: "https://blossom.example/\(String(repeating: "b", count: 64))", - sha256: String(repeating: "b", count: 64), - mediaType: "image/jpeg", - width: 1200, - height: 800, - byteSize: 400, - alt: "Carrots in a basket", - verification: .failed + referenceFingerprint: String(repeating: "d", count: 64), + url: "https://blossom.example/\(String(repeating: "b", count: 64))", + sha256: String(repeating: "b", count: 64), + mediaType: "image/jpeg", + width: 1200, + height: 800, + byteSize: 400, + alt: "Carrots in a basket", + verification: .failed ) let thread = RadrootsThreadEntry( - id: "reply", - authorPublicKey: String(repeating: "c", count: 64), - content: "Are these available Saturday?", - authoredAtUnixSeconds: 1_800_000_001, - type: .reply, - root: "root", - parentEventID: "root", - authorProfile: nil + id: "reply", + authorPublicKey: String(repeating: "c", count: 64), + content: "Are these available Saturday?", + authoredAtUnixSeconds: 1_800_000_001, + type: .reply, + root: "root", + parentEventID: "root", + authorProfile: nil ) let cards = RadrootsTodayCardType.allCases.map { type in makeCard( - id: type.rawValue, - type: type, - profile: profile, - media: type == .photoUpdate ? [failedMedia] : [], - thread: type == .ask ? [thread] : [], - localState: type == .foodAvailability ? "partially_delivered" : nil + id: type.rawValue, + type: type, + profile: profile, + media: type == .photoUpdate ? [failedMedia] : [], + thread: type == .ask ? [thread] : [], + localState: type == .foodAvailability ? "partially_delivered" : nil ) } @@ -139,35 +139,35 @@ final class RadrootsTodayStoreTests: XCTestCase { let context = makeContext(id: "offline", label: "Offline") let cached = makeCard(id: "cached", type: .update) let failure = RadrootsRuntimeFailure( - schemaVersion: 1, - code: "today_relay_offline", - category: "relay", - retryable: true, - recoveryActions: ["retry"], - operationID: "test.today.refresh", - capabilityID: "nostr_source", - safeMessage: "Saved posts are available offline." + schemaVersion: 1, + code: "today_relay_offline", + category: "relay", + retryable: true, + recoveryActions: ["retry"], + operationID: "test.today.refresh", + capabilityID: "nostr_source", + safeMessage: "Saved posts are available offline." ) let backend = TodayBackend( - pages: [ - "offline:first": RadrootsTodayPage( - asOfUnixSeconds: 1_800_000_100, - items: [cached], - nextCursor: "cached-next" - ), - "offline:cached-next": RadrootsTodayPage( - asOfUnixSeconds: 1_800_000_100, - items: [makeCard(id: "cached-next", type: .ask)], - nextCursor: nil - ), - ], - refreshFailure: failure + pages: [ + "offline:first": RadrootsTodayPage( + asOfUnixSeconds: 1_800_000_100, + items: [cached], + nextCursor: "cached-next" + ), + "offline:cached-next": RadrootsTodayPage( + asOfUnixSeconds: 1_800_000_100, + items: [makeCard(id: "cached-next", type: .ask)], + nextCursor: nil + ), + ], + refreshFailure: failure ) let client = try await startedClient(backend: backend) let store = RadrootsTodayStore( - runtimeClient: client, - contexts: [context], - now: { 1_800_000_100 } + runtimeClient: client, + contexts: [context], + now: { 1_800_000_100 } ) await store.reload() @@ -191,83 +191,83 @@ final class RadrootsTodayStoreTests: XCTestCase { private func makeConfiguration() -> RadrootsRuntimeLaunchConfiguration { RadrootsRuntimeLaunchConfiguration( - applicationSupportDirectory: "/tmp/radroots-today-tests", - publicKeyHex: String(repeating: "ab", count: 32), - sourceGenerationHex: String(repeating: "cd", count: 32), - sourceGenerationCreatedAtUnixMilliseconds: 1, - protectedData: .available, - networkProfile: .simulator, - writableRelays: ["ws://127.0.0.1:7447"], - blossom: RadrootsBlossomEndpointConfiguration( - hostKind: .simulator, - endpointAuthority: .loopbackDevelopment, - primaryOrigin: "http://127.0.0.1:3000", - fallbackOrigins: [] - ), - app: RadrootsRuntimeAppMetadata( - bundleIdentifier: "org.radroots.today-tests", - version: "0.1.0-alpha", - buildNumber: "1", - buildSHA: nil - ), - signerGeneration: "today-tests", - signer: TodayTestSigner(), - adoptBootstrapSettings: false + applicationSupportDirectory: "/tmp/radroots-today-tests", + publicKeyHex: String(repeating: "ab", count: 32), + sourceGenerationHex: String(repeating: "cd", count: 32), + sourceGenerationCreatedAtUnixMilliseconds: 1, + protectedData: .available, + networkProfile: .simulator, + writableRelays: ["ws://127.0.0.1:7447"], + blossom: RadrootsBlossomEndpointConfiguration( + hostKind: .simulator, + endpointAuthority: .loopbackDevelopment, + primaryOrigin: "http://127.0.0.1:3000", + fallbackOrigins: [] + ), + app: RadrootsRuntimeAppMetadata( + bundleIdentifier: "org.radroots.today-tests", + version: "0.1.0-alpha", + buildNumber: "1", + buildSHA: nil + ), + signerGeneration: "today-tests", + signer: TodayTestSigner(), + adoptBootstrapSettings: false ) } } private func makeContext(id: String, label: String) -> RadrootsLocalNetwork { RadrootsLocalNetwork( - schemaVersion: 1, - id: id, - label: label, - relayURLs: ["ws://127.0.0.1:7447"], - locality: nil, - followedAuthors: [], - generation: 1 + schemaVersion: 1, + id: id, + label: label, + relayURLs: ["ws://127.0.0.1:7447"], + locality: nil, + followedAuthors: [], + generation: 1 ) } private func makeCard( - id: String, - type: RadrootsTodayCardType, - authoredAt: UInt64 = 1_800_000_000, - profile: RadrootsProfileSummary? = nil, - media: [RadrootsMediaReference] = [], - thread: [RadrootsThreadEntry] = [], - localState: String? = nil + id: String, + type: RadrootsTodayCardType, + authoredAt: UInt64 = 1_800_000_000, + profile: RadrootsProfileSummary? = nil, + media: [RadrootsMediaReference] = [], + thread: [RadrootsThreadEntry] = [], + localState: String? = nil ) -> RadrootsTodayCard { RadrootsTodayCard( - id: id, - type: type, - sourceEventID: "event-\(id)", - sourceAddress: type == .event || type == .foodAvailability ? "address-\(id)" : nil, - authorPublicKey: String(repeating: "a", count: 64), - contractID: "contract-\(type.rawValue)", - title: type == .event ? "Saturday market" : type == .foodAvailability ? "Carrots" : nil, - content: type == .ask ? "Who has seedlings?" : "Fresh from the field", - authoredAtUnixSeconds: authoredAt, - effectiveAtUnixSeconds: authoredAt, - eventStartUnixSeconds: type == .event ? authoredAt + 3600 : nil, - eventEndUnixSeconds: nil, - location: type == .event || type == .foodAvailability ? "Town square" : nil, - priceAmount: type == .foodAvailability ? "3" : nil, - priceCurrency: type == .foodAvailability ? "CAD" : nil, - priceUnit: type == .foodAvailability ? "lb" : nil, - quantity: type == .foodAvailability ? "12" : nil, - foodSummary: type == .foodAvailability ? "Fresh carrots" : nil, - foodPublishedAtUnixSeconds: type == .foodAvailability ? authoredAt : nil, - foodStatus: type == .foodAvailability ? "active" : nil, - contextRank: 1, - inclusionReason: "locality_missing_fallback", - media: media, - lifecycle: .active, - rankDigest: "rank-\(id)", - authorProfile: profile, - thread: thread, - localOperationID: localState == nil ? nil : "operation-\(id)", - localOperationState: localState + id: id, + type: type, + sourceEventID: "event-\(id)", + sourceAddress: type == .event || type == .foodAvailability ? "address-\(id)" : nil, + authorPublicKey: String(repeating: "a", count: 64), + contractID: "contract-\(type.rawValue)", + title: type == .event ? "Saturday market" : type == .foodAvailability ? "Carrots" : nil, + content: type == .ask ? "Who has seedlings?" : "Fresh from the field", + authoredAtUnixSeconds: authoredAt, + effectiveAtUnixSeconds: authoredAt, + eventStartUnixSeconds: type == .event ? authoredAt + 3600 : nil, + eventEndUnixSeconds: nil, + location: type == .event || type == .foodAvailability ? "Town square" : nil, + priceAmount: type == .foodAvailability ? "3" : nil, + priceCurrency: type == .foodAvailability ? "CAD" : nil, + priceUnit: type == .foodAvailability ? "lb" : nil, + quantity: type == .foodAvailability ? "12" : nil, + foodSummary: type == .foodAvailability ? "Fresh carrots" : nil, + foodPublishedAtUnixSeconds: type == .foodAvailability ? authoredAt : nil, + foodStatus: type == .foodAvailability ? "active" : nil, + contextRank: 1, + inclusionReason: "locality_missing_fallback", + media: media, + lifecycle: .active, + rankDigest: "rank-\(id)", + authorProfile: profile, + thread: thread, + localOperationID: localState == nil ? nil : "operation-\(id)", + localOperationState: localState ) } @@ -288,9 +288,9 @@ private actor TodayBackend: RadrootsRuntimeBackend { private var closed = false init( - pages: [String: RadrootsTodayPage], - delays: [String: UInt64] = [:], - refreshFailure: RadrootsRuntimeFailure? = nil + pages: [String: RadrootsTodayPage], + delays: [String: UInt64] = [:], + refreshFailure: RadrootsRuntimeFailure? = nil ) { self.pages = pages self.delays = delays @@ -299,16 +299,16 @@ private actor TodayBackend: RadrootsRuntimeBackend { func snapshot() -> RadrootsRuntimeSnapshot { RadrootsRuntimeSnapshot( - identity: RadrootsRuntimeIdentity( - publicKeyHex: String(repeating: "ab", count: 32), - hostSignerConfigured: true - ), - relay: nil, - blossomConfiguration: nil, - blossomEvidence: nil, - crateName: "radroots_mobile_ffi", - crateVersion: "0.1.0-alpha", - isClosed: closed + identity: RadrootsRuntimeIdentity( + publicKeyHex: String(repeating: "ab", count: 32), + hostSignerConfigured: true + ), + relay: nil, + blossomConfiguration: nil, + blossomEvidence: nil, + crateName: "radroots_mobile_ffi", + crateVersion: "0.1.0-alpha", + isClosed: closed ) } @@ -319,36 +319,36 @@ private actor TodayBackend: RadrootsRuntimeBackend { let key = "\(request.context.id):\(request.cursor ?? "first")" guard let page = pages[key] else { throw RadrootsRuntimeFailure.local( - operation: "test.today.page", - code: "test.page_missing", - safeMessage: "The requested test page is missing." + operation: "test.today.page", + code: "test.page_missing", + safeMessage: "The requested test page is missing." ) } return page } func refreshToday( - context _: RadrootsLocalNetwork, - nowUnixSeconds _: UInt64, - update: RadrootsTodayProjectionUpdate + context _: RadrootsLocalNetwork, + nowUnixSeconds _: UInt64, + update: RadrootsTodayProjectionUpdate ) throws -> RadrootsTodayRefreshReceipt { if let refreshFailure { throw refreshFailure } return RadrootsTodayRefreshReceipt( - update: update, - sourceEvents: 0, - visibleCards: 0, - profiles: 0, - threadEntries: 0, - contentGeneration: 1, - changed: false + update: update, + sourceEvents: 0, + visibleCards: 0, + profiles: 0, + threadEntries: 0, + contentGeneration: 1, + changed: false ) } func subscribe( - bufferCapacity _: Int, - receive _: @escaping @Sendable (RadrootsRuntimeChange) async -> Void + bufferCapacity _: Int, + receive _: @escaping @Sendable (RadrootsRuntimeChange) async -> Void ) -> any RadrootsRuntimeSubscriptionToken { TodaySubscriptionToken() } diff --git a/RadrootsUITests/RadrootsRemoteQualificationUITests.swift b/RadrootsUITests/RadrootsRemoteQualificationUITests.swift @@ -34,7 +34,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { app.launch() let failureCode = app.descendants(matching: .any)["radroots.runtime.failure_code"] - for _ in 0..<8 where !failureCode.exists { + for _ in 0 ..< 8 where !failureCode.exists { for identifier in [ "radroots.identity.create", "radroots.identity.unlock", @@ -69,13 +69,34 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { guard preparePhotoUpdate(app, marker: marker) != nil else { return } let save = app.buttons["radroots.add.save"] scrollTo(app, element: save) - XCTAssertTrue(save.waitForExistence(timeout: 10)) - XCTAssertTrue(waitUntilHittable(save, timeout: 10)) + guard save.waitForExistence(timeout: 10), + waitUntilHittable(save, timeout: 10), + isUnobscured(save, above: app.tabBars.firstMatch) + else { + return XCTFail("The prepared Photo update could not be saved through the visible UI") + } + let addRoot = app.descendants(matching: .any)["radroots.add.root"] save.tap() let saved = app.staticTexts.matching(identifier: "radroots.add.status").firstMatch - XCTAssertTrue( - waitForLabel(saved, label: "Draft saved on this device.", timeout: 20) - ) + let saveStarted = NSPredicate { _, _ in + addRoot.value as? String == "Working" + || saved.exists && saved.label == "Draft saved on this device." + } + let startExpectation = XCTNSPredicateExpectation(predicate: saveStarted, object: app) + guard XCTWaiter.wait(for: [startExpectation], timeout: 10) == .completed else { + return XCTFail("The visible Save draft action did not start durable persistence") + } + let saveSettled = NSPredicate { _, _ in + addRoot.value as? String == "Ready" + } + let settleExpectation = XCTNSPredicateExpectation(predicate: saveSettled, object: app) + guard XCTWaiter.wait(for: [settleExpectation], timeout: 60) == .completed else { + return XCTFail("The visible Save draft action did not finish durable persistence") + } + scrollAddFormToTop(app) + guard waitForLabel(saved, label: "Draft saved on this device.", timeout: 10) else { + return XCTFail("The visible Add status did not confirm the durable draft save") + } XCTAssertTrue(assertUnverifiedDraft(app)) app.terminate() @@ -115,10 +136,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { guard openAdd(app) != nil, openDrafts(app) else { return XCTFail("The durable outbox was unavailable after the final relaunch") } - XCTAssertGreaterThanOrEqual( - app.buttons.matching(NSPredicate(format: "label == 'View'")).count, - 5 - ) + assertDraftOutboxContainsAtLeast(app, count: expected.count) app.buttons["Done"].tap() } @@ -375,7 +393,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { XCUIDevice.shared.press(.home) Thread.sleep(forTimeInterval: 60) - for _ in 0..<24 { + for _ in 0 ..< 24 { app.activate() if app.tabBars.firstMatch.waitForExistence(timeout: 5) { return @@ -443,7 +461,8 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { if issue.auditType == .contrast, let element = issue.element, element.identifier.isEmpty, - element.label.isEmpty { + element.label.isEmpty + { return true } if issue.auditType == .contrast, @@ -474,12 +493,16 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { let navigationBar = app.navigationBars.firstMatch if navigationBar.exists { let boundary = navigationBar.frame.maxY - if frame.minY < boundary && frame.maxY > boundary { return true } + if frame.minY < boundary, frame.maxY > boundary { + return true + } } let tabBar = app.tabBars.firstMatch if tabBar.exists { let boundary = tabBar.frame.minY - if frame.minY < boundary && frame.maxY > boundary { return true } + if frame.minY < boundary, frame.maxY > boundary { + return true + } } return false } @@ -529,38 +552,38 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { case "radroots.add.content": switch type { case "Update": - return "What’s happening locally?" + "What’s happening locally?" case "Photo update": - return "What should neighbors know?" + "What should neighbors know?" case "Ask": - return "What do you need or want to know?" + "What do you need or want to know?" case "Event": - return "Event details (optional)" + "Event details (optional)" case "Food availability": - return "Details" + "Details" default: preconditionFailure("Unrecognized Add type: \(type)") } case "radroots.add.title": - return type == "Food availability" ? "Food" : "Title" + type == "Food availability" ? "Food" : "Title" case "radroots.add.summary": - return "Short summary" + "Short summary" case "radroots.add.location": - return type == "Food availability" ? "Location" : "Location (optional)" + type == "Food availability" ? "Location" : "Location (optional)" case "radroots.add.event_timing": - return "When, Specific time" + "When, Specific time" case "radroots.add.event.start": - return "Starts" + "Starts" case "radroots.add.event.end": - return "Ends" + "Ends" case "radroots.add.price": - return "Price" + "Price" case "radroots.add.currency": - return "Currency" + "Currency" case "radroots.add.unit": - return "Unit, Choose a unit" + "Unit, Choose a unit" case "radroots.add.quantity": - return "Quantity available (optional)" + "Quantity available (optional)" default: preconditionFailure("Unrecognized Add field identifier: \(identifier)") } @@ -610,7 +633,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { @MainActor private func reachRoot(_ app: XCUIApplication) { - for _ in 0..<6 { + for _ in 0 ..< 6 { if app.tabBars.firstMatch.waitForExistence(timeout: 3) { return } @@ -644,7 +667,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { } let meList = meSheet.descendants(matching: .collectionView).firstMatch let settings = app.descendants(matching: .any)["radroots.support.settings"] - for _ in 0..<8 where !settings.exists { + for _ in 0 ..< 8 where !settings.exists { meList.swipeUp() } guard settings.waitForExistence(timeout: 20) else { @@ -689,7 +712,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { account.tap() let settings = app.descendants(matching: .any)["radroots.support.settings"] - for _ in 0..<5 where !settings.exists { + for _ in 0 ..< 5 where !settings.exists { app.swipeUp() } guard settings.waitForExistence(timeout: 20) else { return "settings=unavailable" } @@ -704,7 +727,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { let serverErrorCode = app.descendants(matching: .any)[ "radroots.settings.blossom.server_error_code" ] - for _ in 0..<6 where !httpStatus.exists || !errorCode.exists || !serverErrorCode.exists { + for _ in 0 ..< 6 where !httpStatus.exists || !errorCode.exists || !serverErrorCode.exists { app.swipeUp() } _ = httpStatus.waitForExistence(timeout: 10) @@ -720,7 +743,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { let add = app.tabBars.buttons["Add"] guard add.waitForExistence(timeout: 10) else { return nil } let type = app.descendants(matching: .any)["radroots.add.type"] - for _ in 0..<3 { + for _ in 0 ..< 3 { add.tap() if type.waitForExistence(timeout: 10) { return type @@ -739,7 +762,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { XCTAssertTrue(newDraft.waitForExistence(timeout: 10)) newDraft.tap() - for _ in 0..<3 where !app.buttons["Photo update"].exists { + for _ in 0 ..< 3 where !app.buttons["Photo update"].exists { type.tap() _ = app.buttons["Photo update"].waitForExistence(timeout: 10) } @@ -755,7 +778,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { let content = app.descendants(matching: .any)["radroots.add.content"] var selectedPhotoUpdate = false - for _ in 0..<3 where !selectedPhotoUpdate { + for _ in 0 ..< 3 where !selectedPhotoUpdate { photoUpdate.tap() selectedPhotoUpdate = waitForValue(type, value: "Photo update", timeout: 10) if !selectedPhotoUpdate, !photoUpdate.exists { @@ -780,11 +803,25 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { XCTAssertFalse(app.keyboards.firstMatch.waitForExistence(timeout: 5)) let library = app.descendants(matching: .any)["radroots.add.media.library"] - XCTAssertTrue(library.waitForExistence(timeout: 10)) - XCTAssertTrue(library.isEnabled) + scrollTo(app, element: library) + guard library.waitForExistence(timeout: 10), waitUntilHittable(library, timeout: 10) else { + XCTFail("The Photo Library action did not become available through the visible UI") + return nil + } library.tap() + let preparedStatus = app.staticTexts.matching( + NSPredicate(format: "label == 'Photo prepared. Add descriptive text before publishing.'") + ).firstMatch + guard preparedStatus.waitForExistence(timeout: 60) else { + XCTFail("The selected Photo update did not report the visible prepared state") + return nil + } let prepared = app.descendants(matching: .any)["radroots.add.media.prepared"] - XCTAssertTrue(prepared.waitForExistence(timeout: 30)) + scrollTo(app, element: prepared) + guard prepared.waitForExistence(timeout: 10) else { + XCTFail("The selected Photo update did not reach the visible prepared state") + return nil + } return readySubmit(app) } @@ -1056,7 +1093,12 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { @MainActor private func scrollTo(_ app: XCUIApplication, element: XCUIElement) { let root = app.descendants(matching: .any)["radroots.add.root"] - for attempt in 0..<16 where !element.exists || !element.isHittable { + let tabBar = app.tabBars.firstMatch + for attempt in 0 ..< 16 where !isVisibleInAddViewport( + element, + root: root, + above: tabBar + ) { if element.exists { let targetFrame = element.frame let rootFrame = root.frame @@ -1074,9 +1116,27 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { } @MainActor + private func isVisibleInAddViewport( + _ element: XCUIElement, + root: XCUIElement, + above obstruction: XCUIElement + ) -> Bool { + guard element.exists, element.isHittable, root.exists, obstruction.exists else { + return false + } + let frame = element.frame + let rootFrame = root.frame + return frame.height > 0 + && frame.minY >= rootFrame.minY + && frame.maxY <= min(rootFrame.maxY, obstruction.frame.minY) + } + + @MainActor private func scrollAddFormToTop(_ app: XCUIApplication) { let root = app.descendants(matching: .any)["radroots.add.root"] - for _ in 0..<8 { root.swipeDown() } + for _ in 0 ..< 8 { + root.swipeDown() + } } @MainActor @@ -1094,11 +1154,13 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { throw QualificationError.missingProductSurface } for marker in markers.reversed() { - for _ in 0..<8 { feed.swipeDown() } + for _ in 0 ..< 8 { + feed.swipeDown() + } let card = app.descendants(matching: .any).matching( NSPredicate(format: "label CONTAINS %@", marker) ).firstMatch - for _ in 0..<6 where !card.exists { + for _ in 0 ..< 6 where !card.exists { feed.swipeUp() } guard card.waitForExistence(timeout: 30) else { @@ -1112,7 +1174,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { private func readySubmit(_ app: XCUIApplication) -> XCUIElement? { let submit = app.descendants(matching: .any)["radroots.add.submit"] let addRoot = app.descendants(matching: .any)["radroots.add.root"] - for _ in 0..<8 { + for _ in 0 ..< 8 { if isUnobscured(submit, above: app.tabBars.firstMatch) { break } @@ -1196,6 +1258,39 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { } @MainActor + private func assertDraftOutboxContainsAtLeast(_ app: XCUIApplication, count: Int) { + let sheet = app.descendants(matching: .any)["radroots.add.drafts.sheet"] + let list = sheet.descendants(matching: .collectionView).firstMatch + guard list.waitForExistence(timeout: 10) else { + return XCTFail("The visible Drafts list was unavailable") + } + + for _ in 0 ..< 8 { + list.swipeDown() + } + var identifiers = Set<String>() + let rows = app.descendants(matching: .any).matching( + NSPredicate(format: "identifier BEGINSWITH 'radroots.add.draft.'") + ) + for _ in 0 ..< 12 where identifiers.count < count { + for index in 0 ..< rows.count { + let identifier = rows.element(boundBy: index).identifier + if !identifier.contains(".media_status.") { + identifiers.insert(identifier) + } + } + if identifiers.count < count { + list.swipeUp() + } + } + XCTAssertGreaterThanOrEqual( + identifiers.count, + count, + "The visible Drafts list omitted persisted outbox rows; identifiers=\(identifiers.sorted())" + ) + } + + @MainActor private func waitForWorkToFinish( _ app: XCUIApplication, submit: XCUIElement, @@ -1258,7 +1353,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { let drafts = app.buttons["radroots.add.drafts"] guard drafts.waitForExistence(timeout: 10) else { return false } let sheet = app.descendants(matching: .any)["radroots.add.drafts.sheet"] - for _ in 0..<3 { + for _ in 0 ..< 3 { drafts.tap() if sheet.waitForExistence(timeout: 10) { return true @@ -1277,7 +1372,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase { @MainActor private func focusForTextInput(_ element: XCUIElement, timeout: TimeInterval) -> Bool { let focused = NSPredicate(format: "hasKeyboardFocus == true") - for _ in 0..<3 { + for _ in 0 ..< 3 { element.tap() let expectation = XCTNSPredicateExpectation(predicate: focused, object: element) if XCTWaiter.wait(for: [expectation], timeout: timeout / 3) == .completed { diff --git a/RadrootsUITests/RadrootsRootShellUITests.swift b/RadrootsUITests/RadrootsRootShellUITests.swift @@ -39,13 +39,13 @@ final class RadrootsRootShellUITests: XCTestCase { tabBar.buttons["Today"].tap() XCTAssertTrue(today.waitForExistence(timeout: 5)) - app.open(try XCTUnwrap(URL(string: "radroots://add"))) + try app.open(XCTUnwrap(URL(string: "radroots://add"))) XCTAssertTrue(add.waitForExistence(timeout: 2)) - app.open(try XCTUnwrap(URL(string: "radroots://today"))) + try app.open(XCTUnwrap(URL(string: "radroots://today"))) XCTAssertTrue(today.waitForExistence(timeout: 2)) - app.open(try XCTUnwrap(URL(string: "radroots://add/extra"))) + try app.open(XCTUnwrap(URL(string: "radroots://add/extra"))) XCTAssertTrue(today.waitForExistence(timeout: 2)) XCTAssertFalse(add.exists) } diff --git a/scripts/linux-shared-rust.sh b/scripts/linux-shared-rust.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ -z "${EXT_BUILD_RUN_ACTIVE:-}" || -z "${EXT_BUILD_PROJECT_DIR:-}" ]]; then + echo "linux-shared-rust: run through cargo extbuild run --" >&2 + exit 1 +fi + +readonly SOURCE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +readonly RUNNER_IMAGE="docker.io/library/rust:1.97.1-slim-trixie@sha256:fc0648ac2962539be80bd424729a20fd80f7b64bfba7e90bbd642aed6c697c5a" +readonly RUNNER_ROOT="$EXT_BUILD_PROJECT_DIR/linux-x86_64" +readonly RUNNER_CARGO_HOME="$RUNNER_ROOT/cargo-home" +readonly RUNNER_TARGET="$RUNNER_ROOT/target" + +command -v docker >/dev/null || { + echo "linux-shared-rust: docker is unavailable" >&2 + exit 1 +} +mkdir -p "$RUNNER_CARGO_HOME" "$RUNNER_TARGET" + +docker run --rm --init --platform linux/amd64 \ + --user "$(id -u):$(id -g)" \ + --volume "$SOURCE_ROOT:/workspace:ro" \ + --volume "$RUNNER_CARGO_HOME:/cargo-home" \ + --volume "$RUNNER_TARGET:/target" \ + --workdir /workspace \ + --env CARGO_HOME=/cargo-home \ + --env CARGO_TARGET_DIR=/target \ + "$RUNNER_IMAGE" \ + /bin/bash -c ' + set -euo pipefail + [[ "$(uname -s)" == "Linux" ]] + [[ "$(uname -m)" == "x86_64" ]] + cargo check --workspace --all-targets --locked + cargo test --workspace --all-targets --locked + ' diff --git a/scripts/swift-quality.sh b/scripts/swift-quality.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +readonly -a SOURCE_PATHS=( + Package.swift + Radroots + RadrootsPublicAPITests + RadrootsTests + RadrootsUITests +) + +command -v swiftformat >/dev/null || { + echo "swift-quality: swiftformat is unavailable" >&2 + exit 1 +} +command -v swiftlint >/dev/null || { + echo "swift-quality: swiftlint is unavailable" >&2 + exit 1 +} + +swiftformat --lint --strict --config .swiftformat "${SOURCE_PATHS[@]}" +swiftlint lint \ + --strict \ + --quiet \ + --no-cache \ + --silence-deprecation-warnings \ + --config .swiftlint.yml \ + "${SOURCE_PATHS[@]}" diff --git a/scripts/verify-package-contract.sh b/scripts/verify-package-contract.sh @@ -87,6 +87,13 @@ grep -Fq 'RadrootsUITests/RadrootsRemoteQualificationUITests/testLocalSocialAcce "$repo_root/scripts/xcode.sh" grep -Fq 'RadrootsUITests/RadrootsRemoteQualificationUITests/testLocalSocialDeterministicPersonas' \ "$repo_root/scripts/xcode.sh" +grep -Fq 'requested_content_size=large' "$repo_root/scripts/xcode.sh" +grep -Fq 'requested_content_size=accessibility-extra-extra-extra-large' \ + "$repo_root/scripts/xcode.sh" +grep -Fq 'xcrun simctl ui "$simulator_id" content_size "$requested_content_size"' \ + "$repo_root/scripts/xcode.sh" +grep -Fq 'xcrun simctl ui "$simulator_id" content_size "$previous_content_size"' \ + "$repo_root/scripts/xcode.sh" grep -Fq 'verify-accessibility' "$repo_root/scripts/local-social-fixture.py" grep -Fq 'verify-persona-fixture' "$repo_root/scripts/local-social-fixture.py" grep -Fq 'verify-persona' "$repo_root/scripts/local-social-fixture.py" @@ -126,6 +133,14 @@ grep -Fq '#if DEBUG' "$repo_root/Radroots/App/RadrootsRemoteQualification.swift" grep -Fq 'case "simulator": runtimeMode = "simulator"' \ "$repo_root/Radroots/App/RadrootsRemoteQualification.swift" test -f "$repo_root/scripts/local-social-fixture.py" +test -f "$repo_root/.swiftformat" +test -f "$repo_root/.swiftlint.yml" +test -x "$repo_root/scripts/swift-quality.sh" +test -x "$repo_root/scripts/linux-shared-rust.sh" +grep -Fq -- '--no-cache' "$repo_root/scripts/swift-quality.sh" +grep -Fq 'swift-quality: doctor' "$repo_root/Makefile" +grep -Fq 'linux-shared-rust: doctor' "$repo_root/Makefile" +grep -Fq 'verify: swift-quality linux-shared-rust' "$repo_root/Makefile" if rg -n \ 'AsyncImage|URLSession\.shared|Data\(contentsOf:[[:space:]]*URL' \ diff --git a/scripts/xcode.sh b/scripts/xcode.sh @@ -165,14 +165,16 @@ case "$operation" in five-flow) test_selector=RadrootsUITests/RadrootsRemoteQualificationUITests/testLocalSocialFiveFlowScenario evidence_command=verify - simulator_id= + simulator_id=${destination##*id=} previous_content_size= + requested_content_size=large ;; accessibility) test_selector=RadrootsUITests/RadrootsRemoteQualificationUITests/testLocalSocialAccessibilitySemantics evidence_command=verify-accessibility simulator_id=${destination##*id=} previous_content_size= + requested_content_size=accessibility-extra-extra-extra-large persona_fixture= persona_result= ;; @@ -181,6 +183,7 @@ case "$operation" in evidence_command=verify-persona simulator_id=${destination##*id=} previous_content_size= + requested_content_size=accessibility-extra-extra-extra-large persona_fixture=test-fixtures/local-social-personas.v1.json persona_result="$XCODE_RESULTS/$result_name.persona-result.json" ;; @@ -261,19 +264,17 @@ case "$operation" in echo "error: local-social fixture readiness timed out" >&2 exit 1 fi - if [[ "$scenario" == accessibility || "$scenario" == persona ]]; then - xcrun simctl boot "$simulator_id" >/dev/null 2>&1 || true - xcrun simctl bootstatus "$simulator_id" -b >/dev/null - previous_content_size=$(xcrun simctl ui "$simulator_id" content_size) - case "$previous_content_size" in - extra-small|small|medium|large|extra-large|extra-extra-large|extra-extra-extra-large|accessibility-medium|accessibility-large|accessibility-extra-large|accessibility-extra-extra-large|accessibility-extra-extra-extra-large) ;; - *) - echo "error: simulator content-size state is unavailable" >&2 - exit 1 - ;; - esac - xcrun simctl ui "$simulator_id" content_size accessibility-extra-extra-extra-large - fi + xcrun simctl boot "$simulator_id" >/dev/null 2>&1 || true + xcrun simctl bootstatus "$simulator_id" -b >/dev/null + previous_content_size=$(xcrun simctl ui "$simulator_id" content_size) + case "$previous_content_size" in + extra-small|small|medium|large|extra-large|extra-extra-large|extra-extra-extra-large|accessibility-medium|accessibility-large|accessibility-extra-large|accessibility-extra-extra-large|accessibility-extra-extra-extra-large) ;; + *) + echo "error: simulator content-size state is unavailable" >&2 + exit 1 + ;; + esac + xcrun simctl ui "$simulator_id" content_size "$requested_content_size" set +e xcodebuild \ -project Radroots.xcodeproj \