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 377bb8b3366381d4ca000e51a681d824b91473ce
parent f1974d8f4e1461a2fb7547843ed43f95eac1609d
Author: triesap <tyson@radroots.org>
Date:   Sat,  8 Aug 2026 22:19:40 +0000

ios: make blossom reconfiguration explicit

Diffstat:
MRadroots/App/AppEntry.swift | 5++++-
MRadroots/App/RadrootsAppModel.swift | 23++++++++++++++++++-----
MRadroots/State/RadrootsConfigurationStore.swift | 220++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
MRadroots/State/RadrootsSessionStore.swift | 39+++++++++++++++++++++++++++++++++++++++
MRadroots/Views/RuntimeStatusView.swift | 14++++++++++++++
MRadrootsTests/RadrootsStateMigrationTests.swift | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 386 insertions(+), 18 deletions(-)

diff --git a/Radroots/App/AppEntry.swift b/Radroots/App/AppEntry.swift @@ -33,7 +33,10 @@ struct AppEntry: View { retry: { Task { await appModel.retry() } }, createIdentity: { Task { await appModel.createIdentity() } }, unlockIdentity: { Task { await appModel.unlockIdentity() } }, - recoverIdentity: { Task { await appModel.recoverIdentity() } } + recoverIdentity: { Task { await appModel.recoverIdentity() } }, + applyConfigurationReconfiguration: { + Task { await appModel.applyConfigurationReconfiguration() } + } ) } } diff --git a/Radroots/App/RadrootsAppModel.swift b/Radroots/App/RadrootsAppModel.swift @@ -55,11 +55,15 @@ final class RadrootsAppModel: ObservableObject { } else { do { let runtimeClient = runtimeClient ?? .production() - self.sessionStore = try .production(runtimeClient: runtimeClient) + let sessionStore = try RadrootsSessionStore.production( + runtimeClient: runtimeClient + ) + let mediaCoordinator = try Self.productionMediaCoordinator() + self.sessionStore = sessionStore todayStore = RadrootsTodayStore(runtimeClient: runtimeClient) addStore = RadrootsAddStore( runtimeClient: runtimeClient, - media: Self.productionMediaCoordinator() + media: mediaCoordinator ) searchStore = RadrootsSearchStore(runtimeClient: runtimeClient) meStore = RadrootsMeStore(runtimeClient: runtimeClient) @@ -118,6 +122,12 @@ final class RadrootsAppModel: ObservableObject { } } + func applyConfigurationReconfiguration() async { + await run(name: "configuration_reconfigure", showsStarting: true) { store in + await store.applyConfigurationReconfiguration() + } + } + func stop() async { stopPresentationWork() await run(name: "stop") { store in await store.stop() } @@ -234,17 +244,20 @@ final class RadrootsAppModel: ObservableObject { case .protectedDataUnavailable: "protected_data_unavailable" case .recoveryRequired: "recovery_required" case .corruptIdentity: "corrupt_identity" + case .configurationReconfigurationRequired: "configuration_reconfiguration_required" case .running: "running" case .stopped: "stopped" case .failed: "failed" } } - private static func productionMediaCoordinator(bundle: Bundle = .main) -> RadrootsAddMediaCoordinator? { + private static func productionMediaCoordinator( + bundle: Bundle = .main + ) throws -> RadrootsAddMediaCoordinator { guard let bundleIdentifier = bundle.bundleIdentifier else { - return nil + throw RadrootsConfigurationError.missing("bundle_identifier") } - return try? RadrootsAddMediaCoordinator.production(bundleIdentifier: bundleIdentifier) + return try RadrootsAddMediaCoordinator.production(bundleIdentifier: bundleIdentifier) } } diff --git a/Radroots/State/RadrootsConfigurationStore.swift b/Radroots/State/RadrootsConfigurationStore.swift @@ -1,4 +1,5 @@ import Darwin +import CryptoKit import Foundation import RadrootsKit import Security @@ -24,6 +25,14 @@ struct RadrootsAppConfiguration: Sendable, Equatable { let keychainServicePrefix: String let bundleIdentifier: String let appMetadata: RadrootsRuntimeAppMetadata + let generation: UInt64 + let activationState: RadrootsConfigurationActivationState + let previousBlossomConfigFingerprint: String? +} + +enum RadrootsConfigurationActivationState: String, Codable, Sendable, Equatable { + case current + case reconfigurationRequired = "reconfiguration_required" } struct RadrootsConfigurationBootstrap: Sendable, Equatable { @@ -67,13 +76,24 @@ extension RadrootsConfigurationError: LocalizedError { } actor RadrootsConfigurationStore { - private struct StoredConfigurationV3: Codable { + private struct StoredConfigurationV3: Codable, Equatable { static let format = "radroots_ios_configuration_v3" let format: String let profile: RadrootsAppNetworkProfile let writableRelays: [String] let blossom: RadrootsBlossomEndpointConfiguration? + let generation: UInt64? + let activationState: RadrootsConfigurationActivationState? + let bootstrapFingerprint: String? + let canonicalBlossomConfigFingerprint: String? + let previousBlossomConfigFingerprint: String? + } + + private struct BootstrapNetworkIdentity: Codable { + let profile: RadrootsAppNetworkProfile + let relayURLs: [String] + let blossomOrigins: [String] } private struct StoredConfigurationV2: Codable { @@ -119,17 +139,34 @@ actor RadrootsConfigurationStore { func load() throws -> RadrootsAppConfiguration { let profile = try Self.profile(for: bootstrap.runtimeMode) + let bootstrapFingerprint = try Self.bootstrapFingerprint(bootstrap, profile: profile) let selected: StoredConfigurationV3 if let stored = try readStoredConfiguration() { guard stored.format == StoredConfigurationV3.format else { throw RadrootsConfigurationError.corruptStoredConfiguration } - if stored.profile == profile, - Self.blossomMatchesProfile(stored.blossom, profile: profile) + let upgraded = try Self.upgradeStoredConfiguration( + stored, + bootstrapFingerprint: bootstrapFingerprint + ) + if upgraded.profile == profile, + Self.blossomMatchesProfile(upgraded.blossom, profile: profile), + upgraded.bootstrapFingerprint == bootstrapFingerprint { - selected = stored + selected = upgraded + if selected != stored { + try persist(selected) + } } else { - selected = Self.bootstrapConfiguration(bootstrap, profile: profile) + selected = Self.bootstrapConfiguration( + bootstrap, + profile: profile, + generation: (upgraded.generation ?? 0) + 1, + activationState: .reconfigurationRequired, + bootstrapFingerprint: bootstrapFingerprint, + previousBlossomConfigFingerprint: upgraded.canonicalBlossomConfigFingerprint + ?? upgraded.previousBlossomConfigFingerprint + ) try persist(selected) } } else if let stored = try readV2Configuration() { @@ -144,10 +181,21 @@ actor RadrootsConfigurationStore { blossom: Self.blossomConfiguration( origins: stored.blossomOrigins, profile: profile - ) + ), + generation: 1, + activationState: .current, + bootstrapFingerprint: bootstrapFingerprint, + canonicalBlossomConfigFingerprint: nil, + previousBlossomConfigFingerprint: nil ) } else { - Self.bootstrapConfiguration(bootstrap, profile: profile) + Self.bootstrapConfiguration( + bootstrap, + profile: profile, + generation: 1, + activationState: .reconfigurationRequired, + bootstrapFingerprint: bootstrapFingerprint + ) } try persist(selected) } else if let legacy = try readLegacyConfiguration() { @@ -161,11 +209,22 @@ actor RadrootsConfigurationStore { 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) + selected = Self.bootstrapConfiguration( + bootstrap, + profile: profile, + generation: 1, + activationState: .current, + bootstrapFingerprint: bootstrapFingerprint + ) try persist(selected) } @@ -184,7 +243,63 @@ actor RadrootsConfigurationStore { blossom: selected.blossom, keychainServicePrefix: bootstrap.keychainServicePrefix, bundleIdentifier: bootstrap.bundleIdentifier, - appMetadata: bootstrap.appMetadata + appMetadata: bootstrap.appMetadata, + generation: selected.generation ?? 1, + activationState: selected.activationState ?? .current, + previousBlossomConfigFingerprint: selected.previousBlossomConfigFingerprint + ) + } + + func confirmCanonicalBlossomConfiguration( + _ canonical: RadrootsBlossomConfigurationStatus?, + expectedGeneration: UInt64 + ) throws { + guard let stored = try readStoredConfiguration(), + stored.format == StoredConfigurationV3.format, + stored.generation == expectedGeneration, + let bootstrapFingerprint = stored.bootstrapFingerprint + else { + throw RadrootsConfigurationError.persistenceFailed + } + let canonicalBlossom: RadrootsBlossomEndpointConfiguration? + let canonicalFingerprint: String? + switch (stored.blossom, canonical) { + case (nil, nil): + canonicalBlossom = nil + canonicalFingerprint = nil + case let (.some(selected), .some(canonical)): + guard let hostKind = RadrootsBlossomHostKind(rawValue: canonical.hostKind), + let endpointAuthority = RadrootsBlossomEndpointAuthority( + rawValue: canonical.endpointAuthority + ), + hostKind == selected.hostKind, + endpointAuthority == selected.endpointAuthority, + Self.isFingerprint(canonical.configFingerprint) + else { + throw RadrootsConfigurationError.invalid("canonical_blossom_configuration") + } + canonicalBlossom = RadrootsBlossomEndpointConfiguration( + hostKind: hostKind, + endpointAuthority: endpointAuthority, + primaryOrigin: canonical.primaryOrigin, + fallbackOrigins: canonical.fallbackOrigins + ) + canonicalFingerprint = canonical.configFingerprint + default: + throw RadrootsConfigurationError.invalid("canonical_blossom_configuration") + } + try persist( + StoredConfigurationV3( + format: StoredConfigurationV3.format, + profile: stored.profile, + writableRelays: stored.writableRelays, + blossom: canonicalBlossom, + generation: expectedGeneration, + activationState: .current, + bootstrapFingerprint: bootstrapFingerprint, + canonicalBlossomConfigFingerprint: canonicalFingerprint, + previousBlossomConfigFingerprint: nil + ) ) } @@ -280,16 +395,97 @@ actor RadrootsConfigurationStore { private static func bootstrapConfiguration( _ bootstrap: RadrootsConfigurationBootstrap, - profile: RadrootsAppNetworkProfile + 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) + 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 + ) throws -> StoredConfigurationV3 { + if let generation = stored.generation, + let activationState = stored.activationState, + let persistedBootstrapFingerprint = stored.bootstrapFingerprint + { + guard generation > 0, + isFingerprint(persistedBootstrapFingerprint), + stored.canonicalBlossomConfigFingerprint.map(isFingerprint) ?? true, + stored.previousBlossomConfigFingerprint.map(isFingerprint) ?? true + else { + 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 + ) + } + guard stored.generation == nil, + stored.activationState == nil, + stored.bootstrapFingerprint == nil, + stored.canonicalBlossomConfigFingerprint == nil, + stored.previousBlossomConfigFingerprint == nil + else { + 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 ) } + private static func bootstrapFingerprint( + _ bootstrap: RadrootsConfigurationBootstrap, + profile: RadrootsAppNetworkProfile + ) throws -> String { + do { + let data = try JSONEncoder.radroots.encode( + BootstrapNetworkIdentity( + profile: profile, + relayURLs: bootstrap.relayURLs, + blossomOrigins: bootstrap.blossomOrigins + ) + ) + return SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined() + } catch { + throw RadrootsConfigurationError.invalid("bootstrap_network_identity") + } + } + + private static func isFingerprint(_ value: String) -> Bool { + value.count == 64 + && value == value.lowercased() + && value.allSatisfy(\.isHexDigit) + } + private static func blossomConfiguration( origins: [String], profile: RadrootsAppNetworkProfile diff --git a/Radroots/State/RadrootsSessionStore.swift b/Radroots/State/RadrootsSessionStore.swift @@ -26,11 +26,17 @@ enum RadrootsSessionPhase: Sendable, Equatable { case protectedDataUnavailable(RadrootsAppIdentity) case recoveryRequired(RadrootsAppIdentity) case corruptIdentity(RadrootsAppIdentity) + case configurationReconfigurationRequired(RadrootsConfigurationReconfigurationRequirement) case running(RadrootsRuntimeSnapshot) case stopped case failed(RadrootsRuntimeFailure) } +struct RadrootsConfigurationReconfigurationRequirement: Sendable, Equatable { + let generation: UInt64 + let previousBlossomConfigFingerprint: String? +} + actor RadrootsSessionStore { private let configurationStore: RadrootsConfigurationStore private let identityStore: RadrootsIdentityStore @@ -106,6 +112,14 @@ actor RadrootsSessionStore { } func start() async -> RadrootsSessionPhase { + await start(acceptingReconfiguration: false) + } + + func applyConfigurationReconfiguration() async -> RadrootsSessionPhase { + await start(acceptingReconfiguration: true) + } + + private func start(acceptingReconfiguration: Bool) async -> RadrootsSessionPhase { generation &+= 1 let requestedGeneration = generation phase = .starting @@ -127,6 +141,18 @@ actor RadrootsSessionStore { phase = .corruptIdentity(identity) case .unlocked: let configuration = try await configurationStore.load() + if configuration.activationState == .reconfigurationRequired, + !acceptingReconfiguration + { + phase = .configurationReconfigurationRequired( + RadrootsConfigurationReconfigurationRequirement( + generation: configuration.generation, + previousBlossomConfigFingerprint: configuration + .previousBlossomConfigFingerprint + ) + ) + return phase + } phase = try await startRuntime( configuration: configuration, identity: identity, @@ -252,6 +278,19 @@ actor RadrootsSessionStore { _ = try? await runtimeClient.stop() throw RadrootsRuntimeClientError.superseded } + do { + try await configurationStore.confirmCanonicalBlossomConfiguration( + snapshot.blossomConfiguration, + expectedGeneration: configuration.generation + ) + } catch { + _ = try? await runtimeClient.stop() + throw error + } + guard generation == requestedGeneration else { + _ = try? await runtimeClient.stop() + throw RadrootsRuntimeClientError.superseded + } return .running(snapshot) } diff --git a/Radroots/Views/RuntimeStatusView.swift b/Radroots/Views/RuntimeStatusView.swift @@ -6,6 +6,7 @@ struct RuntimeStatusView: View { let createIdentity: () -> Void let unlockIdentity: () -> Void let recoverIdentity: () -> Void + let applyConfigurationReconfiguration: () -> Void var body: some View { NavigationStack { @@ -33,6 +34,10 @@ struct RuntimeStatusView: View { Button("Recover identity", action: recoverIdentity) .buttonStyle(.borderedProminent) .accessibilityIdentifier("radroots.identity.recover") + } else if case .configurationReconfigurationRequired = phase { + Button("Apply network configuration", action: applyConfigurationReconfiguration) + .buttonStyle(.borderedProminent) + .accessibilityIdentifier("radroots.configuration.reconfigure") } else if case .failed = phase { Button("Retry", action: retry) .buttonStyle(.borderedProminent) @@ -54,6 +59,7 @@ struct RuntimeStatusView: View { case .protectedDataUnavailable: "lock.iphone" case .recoveryRequired: "wrench.and.screwdriver" case .corruptIdentity: "exclamationmark.shield" + case .configurationReconfigurationRequired: "arrow.triangle.2.circlepath" case .running: "checkmark.circle" case .failed: "exclamationmark.triangle" case .stopped: "pause.circle" @@ -63,6 +69,7 @@ struct RuntimeStatusView: View { private var symbolColor: Color { switch phase { case .failed, .corruptIdentity: .red + case .configurationReconfigurationRequired: .orange case .running: .green default: .accentColor } @@ -76,6 +83,7 @@ struct RuntimeStatusView: View { case .protectedDataUnavailable: "Unlock this device" case .recoveryRequired: "Recover your identity" case .corruptIdentity: "Identity data needs repair" + case .configurationReconfigurationRequired: "Network configuration changed" case .running: "Radroots is ready" case .failed: "Radroots needs attention" case .stopped: "Radroots is paused" @@ -96,6 +104,12 @@ struct RuntimeStatusView: View { identity.recoveryCode ?? "A previous identity operation needs recovery." case let .corruptIdentity(identity): identity.recoveryCode ?? "Stored identity state is corrupt; it was not treated as absent." + case let .configurationReconfigurationRequired(requirement): + if let fingerprint = requirement.previousBlossomConfigFingerprint { + "Review and apply the new network configuration. Existing uploads remain bound to configuration \(fingerprint.prefix(12))…." + } else { + "Review and apply the new network configuration. Existing local identity and drafts are preserved." + } case let .running(snapshot): "Runtime \(snapshot.crateVersion) is connected to your local data." case let .failed(failure): diff --git a/RadrootsTests/RadrootsStateMigrationTests.swift b/RadrootsTests/RadrootsStateMigrationTests.swift @@ -123,6 +123,21 @@ final class RadrootsStateMigrationTests: XCTestCase { let blossom = try XCTUnwrap(persisted["blossom"] as? [String: Any]) XCTAssertEqual(blossom["hostKind"] as? String, "simulator") XCTAssertEqual(blossom["endpointAuthority"] as? String, "loopback_development") + XCTAssertEqual(persisted["activationState"] as? String, "current") + XCTAssertEqual(persisted["generation"] as? UInt64, 1) + + 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 + ) + let confirmed = try configurationObject(fileAccess) + XCTAssertEqual(confirmed["canonicalBlossomConfigFingerprint"] as? String, fingerprint) + XCTAssertEqual(confirmed["activationState"] as? String, "current") } func testStoredProfileDriftRecoversFromCurrentBootstrap() async throws { @@ -133,6 +148,15 @@ final class RadrootsStateMigrationTests: XCTestCase { bootstrap: fixture.bootstrap, roots: fixture.roots ).load() + let originalFingerprint = String(repeating: "b", count: 64) + let originalStore = RadrootsConfigurationStore( + bootstrap: fixture.bootstrap, + roots: fixture.roots + ) + try await originalStore.confirmCanonicalBlossomConfiguration( + canonicalBlossomConfiguration(fingerprint: originalFingerprint), + expectedGeneration: 1 + ) let production = RadrootsConfigurationBootstrap( runtimeMode: "production", relayURLs: ["wss://write.example"], @@ -146,6 +170,9 @@ final class RadrootsStateMigrationTests: XCTestCase { let recovered = try await store.load() XCTAssertEqual(recovered.profile, .publicNetwork) + XCTAssertEqual(recovered.activationState, .reconfigurationRequired) + XCTAssertEqual(recovered.generation, 2) + XCTAssertEqual(recovered.previousBlossomConfigFingerprint, originalFingerprint) XCTAssertEqual(recovered.writableRelays, ["wss://write.example"]) XCTAssertEqual( recovered.blossom, @@ -161,6 +188,67 @@ final class RadrootsStateMigrationTests: XCTestCase { let persisted = try configurationObject(fileAccess) XCTAssertEqual(persisted["profile"] as? String, "public") + XCTAssertEqual(persisted["activationState"] as? String, "reconfiguration_required") + XCTAssertNil(persisted["canonicalBlossomConfigFingerprint"]) + XCTAssertEqual( + persisted["previousBlossomConfigFingerprint"] as? String, + originalFingerprint + ) + } + + func testBootstrapInputDriftRequiresExplicitGenerationAwareActivation() async throws { + let fixture = try StateFixture() + defer { fixture.remove() } + let original = RadrootsConfigurationStore( + 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 + ) + 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 + ) + let changed = RadrootsConfigurationStore( + bootstrap: changedBootstrap, + roots: fixture.roots + ) + + let pending = try await changed.load() + + XCTAssertEqual(pending.activationState, .reconfigurationRequired) + XCTAssertEqual(pending.generation, first.generation + 1) + XCTAssertEqual(pending.previousBlossomConfigFingerprint, originalFingerprint) + XCTAssertEqual(pending.writableRelays, ["ws://127.0.0.1:7448"]) + XCTAssertEqual(pending.blossom?.primaryOrigin, "http://127.0.0.1:3001") + + let replacementFingerprint = String(repeating: "d", count: 64) + try await changed.confirmCanonicalBlossomConfiguration( + canonicalBlossomConfiguration( + primaryOrigin: "http://127.0.0.1:3001", + fingerprint: replacementFingerprint + ), + expectedGeneration: pending.generation + ) + let active = try await changed.load() + XCTAssertEqual(active.activationState, .current) + XCTAssertEqual(active.generation, pending.generation) + XCTAssertNil(active.previousBlossomConfigFingerprint) + let persisted = try configurationObject( + RadrootsAppleFileAccess(roots: fixture.roots) + ) + XCTAssertEqual( + persisted["canonicalBlossomConfigFingerprint"] as? String, + replacementFingerprint + ) } func testStoredBlossomAuthorityDriftRecoversWithinProfile() async throws { @@ -213,6 +301,21 @@ final class RadrootsStateMigrationTests: XCTestCase { return object } + private func canonicalBlossomConfiguration( + 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 + ) + } + func testSourceGenerationAndVisualIdentitySurviveStoreRecreation() async throws { let fixture = try StateFixture() defer { fixture.remove() }