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:
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)
+ }
+}