field_ios

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

commit ab5e35352d9ab684a1ecfbd03cda1b2f9683f1b4
parent 6ed849e562b2ed7f6da36fc36c79e4daadb8b78d
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 02:37:50 -0700

test: expose document interchange probe

- add a UI-test-only document interchange probe for export import and share contracts

- surface probe results through hidden accessibility state

- include the probe source in the generated Xcode project

Diffstat:
MRadroots.xcodeproj/project.pbxproj | 4++++
MRadroots/App/AppEntry.swift | 7+++++++
MRadroots/App/AppState.swift | 13+++++++++++++
ARadroots/Runtime/FieldDocumentInterchangeUITestProbe.swift | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 133 insertions(+), 0 deletions(-)

diff --git a/Radroots.xcodeproj/project.pbxproj b/Radroots.xcodeproj/project.pbxproj @@ -47,6 +47,7 @@ DCE468F668A3C346E716B04C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CCF0F7B3C57D8D770F178329 /* Assets.xcassets */; }; E1EDAEE6B182025ACAF754A6 /* RadrootsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15DBA726450712D6DE88E951 /* RadrootsProvider.swift */; }; E3864E34D67BAD0744B93180 /* Bundle+Build.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA7BAA021EE13E829390B /* Bundle+Build.swift */; }; + E432FD39ECC8F03764EEED81 /* FieldDocumentInterchangeUITestProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF05B944D348DAA4EF28B463 /* FieldDocumentInterchangeUITestProbe.swift */; }; EB7C19F62D7DAB9C044D53AA /* PostDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3C0EFACAD213A69C12D5064 /* PostDetailView.swift */; }; F32EFF00A8A852F76657FEE1 /* PostCreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA8AAF0C0F1723860A8481E0 /* PostCreateView.swift */; }; F3E40E5A76B4EA19AC7603D2 /* RadrootsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2DAD90EBF8EB00ACDD7611CD /* RadrootsKit */; }; @@ -101,6 +102,7 @@ E4F2BDC19EF9435162BFF5EC /* FieldDocumentInterchange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldDocumentInterchange.swift; sourceTree = "<group>"; }; E7F9BFC3C8CE2F86FB7DB74B /* DebugDump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugDump.swift; sourceTree = "<group>"; }; E883FDB7004A210C9D7BE27A /* FieldRuntimeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldRuntimeService.swift; sourceTree = "<group>"; }; + EF05B944D348DAA4EF28B463 /* FieldDocumentInterchangeUITestProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldDocumentInterchangeUITestProbe.swift; sourceTree = "<group>"; }; F21554DA87EEC1E5C5F38365 /* PostFeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedViewModel.swift; sourceTree = "<group>"; }; F3C0EFACAD213A69C12D5064 /* PostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailView.swift; sourceTree = "<group>"; }; F4C7DE4207398DE242519F9C /* CopyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyButton.swift; sourceTree = "<group>"; }; @@ -180,6 +182,7 @@ children = ( A71EFADBC7D54AF5B9314773 /* BuildConfig.swift */, E4F2BDC19EF9435162BFF5EC /* FieldDocumentInterchange.swift */, + EF05B944D348DAA4EF28B463 /* FieldDocumentInterchangeUITestProbe.swift */, 491D57E540BAB04F24619737 /* FieldFileAccessUITestProbe.swift */, CA942715DB13FFD494FD35A0 /* FieldIdentityPublicMetadataStore.swift */, 9EB0A2CFCBBFA9D204C6992B /* FieldLocalState.swift */, @@ -410,6 +413,7 @@ D3834AF9A4E1327B7DA557F3 /* CopyRow.swift in Sources */, 4025E63F6603011431B8A0E1 /* DebugDump.swift in Sources */, 3FC570AC038C3DC575E5A3E7 /* FieldDocumentInterchange.swift in Sources */, + E432FD39ECC8F03764EEED81 /* FieldDocumentInterchangeUITestProbe.swift in Sources */, 7E650F6DA30931E310F842E2 /* FieldFileAccessUITestProbe.swift in Sources */, D4CFDE54747B6D6957977025 /* FieldIdentityPublicMetadataStore.swift in Sources */, 6E15D30653861F26AC45B501 /* FieldLocalState.swift in Sources */, diff --git a/Radroots/App/AppEntry.swift b/Radroots/App/AppEntry.swift @@ -36,6 +36,13 @@ public struct AppEntry<Main: View>: View { .accessibilityIdentifier("field_ios.file_access.probe") .accessibilityValue(probeValue) } + if let probeValue = appState.documentInterchangeProbeValue { + Color.clear + .frame(width: 1, height: 1) + .accessibilityElement() + .accessibilityIdentifier("field_ios.document_interchange.probe") + .accessibilityValue(probeValue) + } } } } diff --git a/Radroots/App/AppState.swift b/Radroots/App/AppState.swift @@ -42,6 +42,7 @@ public final class AppState: ObservableObject { @Published public private(set) var relayLight: RelayLight = .red @Published public private(set) var relayLastError: String? @Published public private(set) var fileAccessProbeValue: String? + @Published public private(set) var documentInterchangeProbeValue: String? public var canShowAppContent: Bool { bootstrapPhase == .ready && runtimeIdentityReady && !isLocked @@ -121,6 +122,7 @@ public final class AppState: ObservableObject { resetLocalStateRequested: resetLocalStateRequested, identityResetObserved: false ) + try refreshDocumentInterchangeProbe(bundleIdentifier: appBundleIdentifier) bootstrapPhase = .ready } catch { statusTask?.cancel() @@ -484,6 +486,17 @@ public final class AppState: ObservableObject { } } + private func refreshDocumentInterchangeProbe(bundleIdentifier: String) throws { + documentInterchangeProbeValue = try FieldDocumentInterchangeUITestProbe.startupValue( + bundleIdentifier: bundleIdentifier, + infoJSONString: infoJSONString, + relays: RelaySettings.relays(), + connectedCount: relayConnectedCount, + connectingCount: relayConnectingCount, + lastError: relayLastError + ) + } + private func setLocked(_ value: Bool) { isLocked = value UserDefaults.standard.set(value, forKey: lockKey) diff --git a/Radroots/Runtime/FieldDocumentInterchangeUITestProbe.swift b/Radroots/Runtime/FieldDocumentInterchangeUITestProbe.swift @@ -0,0 +1,109 @@ +import Foundation +import RadrootsKit + +enum FieldDocumentInterchangeUITestProbe { + private static let enabledKey = "RADROOTS_FIELD_IOS_UI_TEST_DOCUMENT_INTERCHANGE_PROBE" + private static let importFixture = RadrootsFileReference( + scope: .temporary, + relativePath: "ui_tests/document_interchange/relay_import.json" + ) + private static let invalidImportFixture = RadrootsFileReference( + scope: .temporary, + relativePath: "ui_tests/document_interchange/invalid_relay_import.json" + ) + + static var isRequested: Bool { + ProcessInfo.processInfo.environment[enabledKey] == "true" + } + + static func startupValue( + bundleIdentifier: String, + infoJSONString: String, + relays: [String], + connectedCount: UInt32, + connectingCount: UInt32, + lastError: String? + ) throws -> String? { + guard isRequested else { + return nil + } + let interchange = try FieldDocumentInterchange(bundleIdentifier: bundleIdentifier) + let fileAccess = try FieldLocalState.fileAccess(bundleIdentifier: bundleIdentifier) + let diagnosticsExport = try interchange.prepareDiagnosticsExport( + infoJSONString: infoJSONString, + relays: relays, + connectedCount: connectedCount, + connectingCount: connectingCount, + lastError: lastError + ) + let diagnosticsFileExists = FileManager.default.fileExists(atPath: diagnosticsExport.fileURL.path) + try fileAccess.releasePreparedExport(diagnosticsExport) + let diagnosticsReleaseRemovedFile = !FileManager.default.fileExists(atPath: diagnosticsExport.fileURL.path) + let relayExport = try interchange.prepareRelayConfigExport(relays: relays) + let relayExportFileExists = FileManager.default.fileExists(atPath: relayExport.fileURL.path) + try fileAccess.releasePreparedExport(relayExport) + try fileAccess.write( + .inline(relayImportFixtureData()), + to: importFixture + ) + let importedRelays = try interchange.importedRelayConfig(from: importFixture) + try fileAccess.write( + .inline(invalidRelayImportFixtureData()), + to: invalidImportFixture + ) + let rejectedInvalidImport = invalidImportWasRejected(interchange) + let shareRequest = try interchange.publicPostShareRequest(content: " public field update ") + let shareTextTrimmed = shareRequest.items == [.text("public field update")] + return [ + "diagnostics_filename=\(diagnosticsExport.suggestedFilename)", + "diagnostics_media_type=\(diagnosticsExport.mediaType ?? "")", + "diagnostics_file_exists=\(diagnosticsFileExists)", + "diagnostics_release_removed_file=\(diagnosticsReleaseRemovedFile)", + "relay_export_filename=\(relayExport.suggestedFilename)", + "relay_export_media_type=\(relayExport.mediaType ?? "")", + "relay_export_file_exists=\(relayExportFileExists)", + "relay_import_count=\(importedRelays.count)", + "relay_import_contains_production=\(importedRelays.contains("wss://radroots.org"))", + "relay_import_rejected_invalid=\(rejectedInvalidImport)", + "share_subject=\(shareRequest.subject ?? "")", + "share_text_trimmed=\(shareTextTrimmed)" + ].joined(separator: ";") + } + + private static func invalidImportWasRejected(_ interchange: FieldDocumentInterchange) -> Bool { + do { + _ = try interchange.importedRelayConfig(from: invalidImportFixture) + return false + } catch { + return true + } + } + + private static func relayImportFixtureData() -> Data { + Data( + """ + { + "format": "radroots_field_ios_relay_config_v1", + "relays": [ + "wss://radroots.org", + "ws://127.0.0.1:8080", + "wss://radroots.org" + ] + } + """.utf8 + ) + } + + private static func invalidRelayImportFixtureData() -> Data { + Data( + """ + { + "format": "radroots_field_ios_relay_config_v1", + "relays": [ + "https://radroots.org" + ] + } + """.utf8 + ) + } +}