field_ios

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

commit e54953ce64691ee034353cae7a652519559e2e61
parent def6c140a95ae67be2034828ca58e7e60f743170
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 01:38:39 -0700

test: expose file access probe

- add a UI-test-only AppleKit file access probe
- report logging and reset sentinel state through accessibility
- keep identity reset scoped to secure identity material
- regenerate the app project for the new probe source

Diffstat:
MRadroots.xcodeproj/project.pbxproj | 4++++
MRadroots/App/AppEntry.swift | 9+++++++++
MRadroots/App/AppState.swift | 43+++++++++++++++++++++++++++++++++++++++++--
ARadroots/Runtime/FieldFileAccessUITestProbe.swift | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 164 insertions(+), 2 deletions(-)

diff --git a/Radroots.xcodeproj/project.pbxproj b/Radroots.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 657BEA5AAFF129E10177FE63 /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63189EB90A86A9929BECD9ED /* Nostr.swift */; }; 6E15D30653861F26AC45B501 /* FieldLocalState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB0A2CFCBBFA9D204C6992B /* FieldLocalState.swift */; }; 7C8DD424F3E3E0AB1B133863 /* RadrootsKitBindings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC881872F750120A184F45E6 /* RadrootsKitBindings.swift */; }; + 7E650F6DA30931E310F842E2 /* FieldFileAccessUITestProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D57E540BAB04F24619737 /* FieldFileAccessUITestProbe.swift */; }; 7FD8FB018DA09568303194B2 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE61264E2C98E73828E8680 /* Strings.swift */; }; 8B923F78BF5B680C7F6A7CE3 /* PostFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE0EB327C10171444553378 /* PostFeedView.swift */; }; 8F6D0970610DF68816DE1A98 /* Radroots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0F21496E7A8490EB14AC5B /* Radroots.swift */; }; @@ -64,6 +65,7 @@ 2FE790CA1CD31208947913B9 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; }; 41A4289F43625DD65E6C4B25 /* CopyRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyRow.swift; sourceTree = "<group>"; }; 466BFA2F60BE3113EDD1BA3B /* TradeOrderRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TradeOrderRequestView.swift; sourceTree = "<group>"; }; + 491D57E540BAB04F24619737 /* FieldFileAccessUITestProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldFileAccessUITestProbe.swift; sourceTree = "<group>"; }; 4BC4B7D0BB4C6D8E4B0AA4AD /* radroots.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = radroots.xcconfig; sourceTree = "<group>"; }; 54EE5A34FE2086899F5B7568 /* radroots.git.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = radroots.git.xcconfig; sourceTree = "<group>"; }; 63189EB90A86A9929BECD9ED /* Nostr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nostr.swift; sourceTree = "<group>"; }; @@ -175,6 +177,7 @@ isa = PBXGroup; children = ( A71EFADBC7D54AF5B9314773 /* BuildConfig.swift */, + 491D57E540BAB04F24619737 /* FieldFileAccessUITestProbe.swift */, CA942715DB13FFD494FD35A0 /* FieldIdentityPublicMetadataStore.swift */, 9EB0A2CFCBBFA9D204C6992B /* FieldLocalState.swift */, E883FDB7004A210C9D7BE27A /* FieldRuntimeService.swift */, @@ -403,6 +406,7 @@ 9121BD4A3E7C6EF2B21F540F /* CopyButton.swift in Sources */, D3834AF9A4E1327B7DA557F3 /* CopyRow.swift in Sources */, 4025E63F6603011431B8A0E1 /* DebugDump.swift in Sources */, + 7E650F6DA30931E310F842E2 /* FieldFileAccessUITestProbe.swift in Sources */, D4CFDE54747B6D6957977025 /* FieldIdentityPublicMetadataStore.swift in Sources */, 6E15D30653861F26AC45B501 /* FieldLocalState.swift in Sources */, D25C1E1DC99F5CF8E99AE970 /* FieldRuntimeService.swift in Sources */, diff --git a/Radroots/App/AppEntry.swift b/Radroots/App/AppEntry.swift @@ -28,6 +28,15 @@ public struct AppEntry<Main: View>: View { } } .accessibilityIdentifier("field_ios.app_entry") + .overlay(alignment: .topLeading) { + if let probeValue = appState.fileAccessProbeValue { + Color.clear + .frame(width: 1, height: 1) + .accessibilityElement() + .accessibilityIdentifier("field_ios.file_access.probe") + .accessibilityValue(probeValue) + } + } } } diff --git a/Radroots/App/AppState.swift b/Radroots/App/AppState.swift @@ -40,6 +40,7 @@ public final class AppState: ObservableObject { @Published public private(set) var relayConnectingCount: UInt32 = 0 @Published public private(set) var relayLight: RelayLight = .red @Published public private(set) var relayLastError: String? + @Published public private(set) var fileAccessProbeValue: String? public var canShowAppContent: Bool { bootstrapPhase == .ready && runtimeIdentityReady && !isLocked @@ -91,10 +92,16 @@ public final class AppState: ObservableObject { let service = try radroots.start() let secureStore = try FieldSecureIdentityStore.configured() let metadataStore = try FieldIdentityPublicMetadataStore.configured() + let appBundleIdentifier = try bundleIdentifier() + let resetLocalStateRequested = BuildConfig.bool(.resetLocalState) == true + try FieldFileAccessUITestProbe.seedDestructiveResetSentinelIfRequested( + bundleIdentifier: appBundleIdentifier, + resetLocalStateRequested: resetLocalStateRequested + ) secureIdentityStore = secureStore identityMetadataStore = metadataStore - if BuildConfig.bool(.resetLocalState) == true { - try FieldLocalState.resetFileRoots(bundleIdentifier: try bundleIdentifier()) + if resetLocalStateRequested { + try FieldLocalState.resetFileRoots(bundleIdentifier: appBundleIdentifier) try secureStore.deleteSelectedSecret() metadataStore.delete() try await resetRuntimeIdentityState(using: service) @@ -108,6 +115,11 @@ public final class AppState: ObservableObject { try await connect(using: service) startPollingStatus() } + try refreshFileAccessProbe( + bundleIdentifier: appBundleIdentifier, + resetLocalStateRequested: resetLocalStateRequested, + identityResetObserved: false + ) bootstrapPhase = .ready } catch { statusTask?.cancel() @@ -189,6 +201,11 @@ public final class AppState: ObservableObject { relayLight = .red relayLastError = nil await refreshRuntimeState(using: service) + try refreshFileAccessProbe( + bundleIdentifier: try bundleIdentifier(), + resetLocalStateRequested: false, + identityResetObserved: true + ) statusTask?.cancel() statusTask = nil } @@ -410,6 +427,28 @@ public final class AppState: ObservableObject { return bundleIdentifier } + private func refreshFileAccessProbe( + bundleIdentifier: String, + resetLocalStateRequested: Bool, + identityResetObserved: Bool + ) throws { + let loggingSettings = LoggingSettings.load() + if identityResetObserved { + fileAccessProbeValue = try FieldFileAccessUITestProbe.identityResetValue( + bundleIdentifier: bundleIdentifier, + loggingFileEnabled: loggingSettings.fileEnabled, + loggingFileName: loggingSettings.fileName + ) + } else { + fileAccessProbeValue = try FieldFileAccessUITestProbe.startupValue( + bundleIdentifier: bundleIdentifier, + resetLocalStateRequested: resetLocalStateRequested, + loggingFileEnabled: loggingSettings.fileEnabled, + loggingFileName: loggingSettings.fileName + ) + } + } + private func setLocked(_ value: Bool) { isLocked = value UserDefaults.standard.set(value, forKey: lockKey) diff --git a/Radroots/Runtime/FieldFileAccessUITestProbe.swift b/Radroots/Runtime/FieldFileAccessUITestProbe.swift @@ -0,0 +1,110 @@ +import Foundation +import RadrootsKit + +enum FieldFileAccessUITestProbe { + private static let enabledKey = "RADROOTS_FIELD_IOS_UI_TEST_FILE_ACCESS_PROBE" + private static let destructiveResetSentinel = RadrootsFileReference( + scope: .data, + relativePath: "ui_tests/file_access/destructive_reset_sentinel.txt" + ) + private static let identityBoundarySentinel = RadrootsFileReference( + scope: .data, + relativePath: "ui_tests/file_access/identity_boundary_sentinel.txt" + ) + + static var isRequested: Bool { + ProcessInfo.processInfo.environment[enabledKey] == "true" + } + + static func seedDestructiveResetSentinelIfRequested( + bundleIdentifier: String, + resetLocalStateRequested: Bool + ) throws { + guard isRequested && resetLocalStateRequested else { + return + } + try access(bundleIdentifier: bundleIdentifier).write( + .inline(Data("destructive-reset-sentinel".utf8)), + to: destructiveResetSentinel + ) + } + + static func startupValue( + bundleIdentifier: String, + resetLocalStateRequested: Bool, + loggingFileEnabled: Bool, + loggingFileName: String + ) throws -> String? { + guard isRequested else { + return nil + } + let fileAccess = try access(bundleIdentifier: bundleIdentifier) + try fileAccess.write( + .inline(Data("identity-boundary-sentinel".utf8)), + to: identityBoundarySentinel + ) + return try value( + bundleIdentifier: bundleIdentifier, + resetLocalStateRequested: resetLocalStateRequested, + identityResetObserved: false, + loggingFileEnabled: loggingFileEnabled, + loggingFileName: loggingFileName + ) + } + + static func identityResetValue( + bundleIdentifier: String, + loggingFileEnabled: Bool, + loggingFileName: String + ) throws -> String? { + guard isRequested else { + return nil + } + return try value( + bundleIdentifier: bundleIdentifier, + resetLocalStateRequested: false, + identityResetObserved: true, + loggingFileEnabled: loggingFileEnabled, + loggingFileName: loggingFileName + ) + } + + private static func value( + bundleIdentifier: String, + resetLocalStateRequested: Bool, + identityResetObserved: Bool, + loggingFileEnabled: Bool, + loggingFileName: String + ) throws -> String { + let fileAccess = try access(bundleIdentifier: bundleIdentifier) + let resetSentinelExists = try exists(destructiveResetSentinel, using: fileAccess) + let identitySentinelExists = try exists(identityBoundarySentinel, using: fileAccess) + let logFileURL = try FieldLocalState.logFileURL(bundleIdentifier: bundleIdentifier, fileName: loggingFileName) + let logsRoot = try FieldLocalState.roots(bundleIdentifier: bundleIdentifier).root(for: .logs) + let logURLUnderLogsRoot = logFileURL.path.hasPrefix(logsRoot.path + "/") + let destructiveResetRemovedSentinel = resetLocalStateRequested ? !resetSentinelExists : true + return [ + "destructive_reset_removed_sentinel=\(destructiveResetRemovedSentinel)", + "identity_boundary_sentinel_exists=\(identitySentinelExists)", + "identity_reset_observed=\(identityResetObserved)", + "logging_file_enabled=\(loggingFileEnabled)", + "log_url_under_logs_root=\(logURLUnderLogsRoot)" + ].joined(separator: ";") + } + + private static func exists( + _ file: RadrootsFileReference, + using fileAccess: RadrootsAppleFileAccess + ) throws -> Bool { + do { + _ = try fileAccess.read(file, mode: .inline) + return true + } catch RadrootsAppleFileError.notFound(_) { + return false + } + } + + private static func access(bundleIdentifier: String) throws -> RadrootsAppleFileAccess { + try FieldLocalState.fileAccess(bundleIdentifier: bundleIdentifier) + } +}