field_ios

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

TeraRemoteQualificationUITests.swift (70417B)


      1 import CryptoKit
      2 import XCTest
      3 
      4 final class TeraRemoteQualificationUITests: XCTestCase {
      5   private static let receiptName = "radroots-remote-qualification-bootstrap.json"
      6 
      7   @MainActor
      8   func testBootstrapIdentityWithoutInteractiveAuthentication() throws {
      9     let configuration = try QualificationConfiguration.environment()
     10     let app = launchToRoot(configuration)
     11     let publicKey = try readPublicKey(app)
     12     try writeBootstrapReceipt(configuration: configuration, publicKey: publicKey)
     13 
     14     XCUIDevice.shared.press(.home)
     15     app.activate()
     16     XCTAssertTrue(app.tabBars.firstMatch.waitForExistence(timeout: 10))
     17 
     18     app.terminate()
     19     app.launch()
     20     reachRoot(app)
     21     XCTAssertTrue(app.tabBars.firstMatch.waitForExistence(timeout: 20))
     22   }
     23 
     24   @MainActor
     25   func testStrictPublicEndpointRejectsPrivateAddress() throws {
     26     let environment = try QualificationConfiguration.environment()
     27     let configuration = QualificationConfiguration(
     28       runID: environment.runID,
     29       relayURLs: [],
     30       blossomOrigins: ["https://127.0.0.1"]
     31     )
     32     let app = XCUIApplication()
     33     app.launchEnvironment = configuration.launchEnvironment
     34     app.launch()
     35 
     36     let failureCode = app.descendants(matching: .any)["radroots.runtime.failure_code"]
     37     for _ in 0 ..< 8 where !failureCode.exists {
     38       for identifier in [
     39         "radroots.identity.create",
     40         "radroots.identity.unlock",
     41         "radroots.configuration.reconfigure",
     42         "radroots.identity.recover",
     43       ] {
     44         let action = app.descendants(matching: .any)[identifier]
     45         if action.exists, action.isHittable {
     46           action.tap()
     47           break
     48         }
     49       }
     50       _ = failureCode.waitForExistence(timeout: 4)
     51     }
     52     XCTAssertEqual(
     53       failureCode.label,
     54       "Error code runtime_failed",
     55       "A private Blossom endpoint did not expose the typed fail-closed result; "
     56         + "failure_code.label=\(failureCode.exists ? failureCode.label : "missing")"
     57     )
     58     XCTAssertFalse(app.tabBars.firstMatch.exists)
     59   }
     60 
     61   @MainActor
     62   func testLocalSocialFiveFlowScenario() throws {
     63     let configuration = try QualificationConfiguration.environment()
     64     let control = try XCTUnwrap(configuration.fixtureControl)
     65     try? FileManager.default.removeItem(atPath: control)
     66     var app = launchToRoot(configuration)
     67     let marker = "\(configuration.runID)-photo"
     68 
     69     guard preparePhotoUpdate(app, marker: marker) != nil else { return }
     70     let save = app.buttons["radroots.add.save"]
     71     scrollTo(app, element: save)
     72     guard save.waitForExistence(timeout: 10),
     73       waitUntilHittable(save, timeout: 10),
     74       isUnobscured(save, above: app.tabBars.firstMatch)
     75     else {
     76       return XCTFail("The prepared Photo update could not be saved through the visible UI")
     77     }
     78     let addRoot = app.descendants(matching: .any)["radroots.add.root"]
     79     save.tap()
     80     scrollAddFormToTop(app)
     81     let saved = app.staticTexts.matching(identifier: "radroots.add.status").firstMatch
     82     guard waitForLabel(saved, label: "Draft saved on this device.", timeout: 60) else {
     83       return XCTFail("The visible Add status did not confirm the durable draft save")
     84     }
     85     let saveSettled = NSPredicate { _, _ in
     86       addRoot.value as? String == "Ready"
     87     }
     88     let settleExpectation = XCTNSPredicateExpectation(predicate: saveSettled, object: app)
     89     guard XCTWaiter.wait(for: [settleExpectation], timeout: 60) == .completed else {
     90       return XCTFail("The visible Save draft action did not finish durable persistence")
     91     }
     92     assertSavedPhotoEditing(app)
     93     app.terminate()
     94 
     95     XCTAssertTrue(FileManager.default.createFile(atPath: control, contents: Data()))
     96     app = launchToRoot(configuration)
     97     guard openAdd(app) != nil, openDrafts(app) else {
     98       return XCTFail("The persisted media draft was unavailable after relaunch")
     99     }
    100     let reopen = app.buttons["Reopen"].firstMatch
    101     XCTAssertTrue(reopen.waitForExistence(timeout: 20))
    102     reopen.tap()
    103     guard let retrySubmit = readySubmit(app),
    104       let retryValue = submitAndWait(app, submit: retrySubmit)
    105     else { return }
    106     XCTAssertTrue(
    107       !retryValue.contains("Error code") && finishOriginalPublication(app, submit: retrySubmit),
    108       "The preserved media draft did not publish after retry; submit.value=\(retryValue)"
    109     )
    110 
    111     let markers = [
    112       "\(configuration.runID)-update",
    113       "\(configuration.runID)-ask",
    114       "\(configuration.runID)-event",
    115       "\(configuration.runID)-food",
    116     ]
    117     try publishTextFlow(app, type: "Update", marker: markers[0])
    118     try publishTextFlow(app, type: "Ask", marker: markers[1])
    119     try publishEvent(app, marker: markers[2])
    120     try publishFood(app, marker: markers[3])
    121 
    122     let expected = [marker] + markers
    123     try assertTodayContains(app, markers: expected)
    124     app.terminate()
    125     app = launchToRoot(configuration)
    126     try assertTodayContains(app, markers: expected)
    127 
    128     guard openAdd(app) != nil, openDrafts(app) else {
    129       return XCTFail("The durable outbox was unavailable after the final relaunch")
    130     }
    131     assertDraftOutboxContainsAtLeast(app, count: expected.count)
    132     app.buttons["Done"].tap()
    133   }
    134 
    135   @MainActor
    136   func testLocalSocialAccessibilitySemantics() throws {
    137     let configuration = try QualificationConfiguration.environment()
    138     _ = TeraAccessibilitySettings.setSystemReduceMotion(true, restoring: self)
    139     let app = launchToRoot(
    140       configuration,
    141       launchArguments: [
    142         "-AppleLanguages", "(en)",
    143         "-AppleLocale", "en_US",
    144       ]
    145     )
    146 
    147     XCTAssertEqual(app.tabBars.firstMatch.buttons.count, 2)
    148     XCTAssertGreaterThan(app.staticTexts["Nothing here yet"].frame.height, 36)
    149     XCTAssertGreaterThan(
    150       app.staticTexts["Pull to refresh or add the first update to this local network."].frame
    151         .height,
    152       44
    153     )
    154     app.tabBars.buttons["Today"].tap()
    155     try performLocalSocialAccessibilityAudit(app)
    156 
    157     for type in ["Update", "Photo update", "Ask", "Event", "Food availability"] {
    158       try beginDraft(app, type: type)
    159       assertProgressiveDisclosure(app, type: type)
    160       scrollTo(app, element: app.buttons["radroots.add.submit"])
    161       try performLocalSocialAccessibilityAudit(app)
    162     }
    163   }
    164 
    165   @MainActor
    166   func testLocalSocialDeterministicPersonas() throws {
    167     let environment = try QualificationConfiguration.environment()
    168     let control = try XCTUnwrap(environment.fixtureControl)
    169     let suite = try loadPersonaSuite()
    170     XCTAssertEqual(suite.locale, "en_US")
    171     XCTAssertEqual(suite.personas.map(\.alias), ["P01", "P02", "P03", "P04", "P05"])
    172 
    173     var identityDigests = Set<String>()
    174     var observations = [String: PersonaAttemptObservation]()
    175     for persona in suite.personas {
    176       try activateFixture(persona: persona.alias, at: control)
    177       let configuration = environment.forPersona(persona.alias)
    178       var networkBefore = try readFixtureNetwork(configuration)
    179       var app = launchPersona(configuration)
    180       let publicKey = try readPublicKey(app)
    181       let identityDigest = SHA256.hash(data: Data(publicKey.utf8))
    182         .map { String(format: "%02x", $0) }.joined()
    183       XCTAssertTrue(identityDigests.insert(identityDigest).inserted)
    184 
    185       for attempt in persona.attempts {
    186         let interaction: PersonaInteractionObservation
    187         switch attempt.expectedFailure {
    188         case "validation_recovery":
    189           interaction = try publishWithValidationRecovery(app, attempt: attempt)
    190         case "transport_retry_relaunch":
    191           let retry = try publishWithTransportRetry(
    192             app,
    193             configuration: configuration,
    194             attempt: attempt
    195           )
    196           app = retry.app
    197           interaction = retry.observation
    198         case "none":
    199           interaction = try publishPersonaAttempt(
    200             app,
    201             attempt: attempt,
    202             interactionProfile: persona.interactionProfile
    203           )
    204         default:
    205           throw QualificationError.invalidPersonaFixture
    206         }
    207         try assertTodayContains(app, markers: [attempt.marker])
    208         let networkAfter = try readFixtureNetwork(configuration)
    209         observations[attempt.id] = try PersonaAttemptObservation(
    210           interaction: interaction,
    211           network: networkAfter.delta(from: networkBefore),
    212           todayProjectionVerified: true,
    213           retentionVerified: false
    214         )
    215         networkBefore = networkAfter
    216       }
    217 
    218       let markers = persona.attempts.map(\.marker)
    219       app.terminate()
    220       app = launchPersona(configuration)
    221       try assertTodayContains(app, markers: markers)
    222       XCTAssertEqual(try readPublicKey(app), publicKey)
    223       for attempt in persona.attempts {
    224         let observation = try XCTUnwrap(observations[attempt.id])
    225         try writePersonaAttemptAttachment(
    226           configuration: configuration,
    227           personaAlias: persona.alias,
    228           attempt: attempt,
    229           identityDigest: identityDigest,
    230           observation: observation.confirmingRetention()
    231         )
    232       }
    233       app.terminate()
    234     }
    235     XCTAssertEqual(identityDigests.count, 5)
    236   }
    237 
    238   @MainActor
    239   func testRemoteBlossomUploadAndRecovery() throws {
    240     let configuration = try QualificationConfiguration.environment()
    241     let app = launchToRoot(configuration)
    242     let marker = "Tera remote qualification \(configuration.runID)"
    243 
    244     guard let submit = preparePhotoUpdate(app, marker: marker),
    245       let terminalSubmitValue = submitAndWait(app, submit: submit)
    246     else { return }
    247     guard !terminalSubmitValue.contains("Error code") else {
    248       return XCTFail("The Add submission failed locally; submit.value=\(terminalSubmitValue)")
    249     }
    250 
    251     guard openDrafts(app) else {
    252       return XCTFail("The Drafts sheet did not open after the upload attempt")
    253     }
    254     let mediaStatus = app.descendants(matching: .any).matching(
    255       NSPredicate(format: "identifier BEGINSWITH 'radroots.add.draft.media_status.'")
    256     ).firstMatch
    257     guard mediaStatus.waitForExistence(timeout: 20) else {
    258       app.buttons["Done"].tap()
    259       let evidence = readBlossomFailureEvidence(app)
    260       XCTFail("Draft media status was unavailable after upload; \(evidence)")
    261       return
    262     }
    263     let mediaStatusLabel = mediaStatus.label
    264     guard mediaStatusLabel.contains("1 of 1 photos verified") else {
    265       app.buttons["Done"].tap()
    266       let evidence = readBlossomFailureEvidence(app)
    267       XCTFail(
    268         "Remote Blossom upload was not verified; media=\(mediaStatusLabel); \(evidence)"
    269       )
    270       return
    271     }
    272     app.buttons["Done"].tap()
    273 
    274     XCUIDevice.shared.press(.home)
    275     app.activate()
    276     XCTAssertTrue(app.tabBars.firstMatch.waitForExistence(timeout: 10))
    277 
    278     app.terminate()
    279     app.launch()
    280     reachRoot(app)
    281     guard openAdd(app) != nil else {
    282       return XCTFail("The Add bottom tab did not recover after relaunch")
    283     }
    284     app.buttons["radroots.add.drafts"].tap()
    285     XCTAssertTrue(
    286       app.descendants(matching: .any).matching(
    287         NSPredicate(format: "label CONTAINS '1 of 1 photos verified'")
    288       ).firstMatch.waitForExistence(timeout: 20)
    289     )
    290     app.buttons["Done"].tap()
    291 
    292     if !configuration.relayURLs.isEmpty {
    293       app.tabBars.buttons["Today"].tap()
    294       let published = app.descendants(matching: .any).matching(
    295         NSPredicate(format: "label CONTAINS %@", marker)
    296       ).firstMatch
    297       XCTAssertTrue(published.waitForExistence(timeout: 30))
    298     }
    299   }
    300 
    301   @MainActor
    302   func testRemoteBlossomUnavailablePersistsDraft() throws {
    303     let configuration = try QualificationConfiguration.environment()
    304     let app = launchToRoot(configuration)
    305     let marker = "Tera unavailable qualification \(configuration.runID)"
    306 
    307     guard let submit = preparePhotoUpdate(app, marker: marker),
    308       let terminalSubmitValue = submitAndWait(app, submit: submit)
    309     else { return }
    310     XCTAssertTrue(
    311       terminalSubmitValue.contains("Error code"),
    312       "An unavailable Blossom endpoint did not produce a typed failure; "
    313         + "submit.value=\(terminalSubmitValue)"
    314     )
    315     XCTAssertTrue(assertUnverifiedDraft(app))
    316 
    317     app.terminate()
    318     app.launch()
    319     reachRoot(app)
    320     guard openAdd(app) != nil else {
    321       return XCTFail("The Add bottom tab did not recover after unavailable relaunch")
    322     }
    323     XCTAssertTrue(assertUnverifiedDraft(app))
    324   }
    325 
    326   @MainActor
    327   func testRemoteBlossomRetriesPersistedDraft() throws {
    328     let configuration = try QualificationConfiguration.environment()
    329     let app = launchToRoot(configuration)
    330     guard openAdd(app) != nil else {
    331       return XCTFail("The Add bottom tab did not present the Add surface")
    332     }
    333     guard openDrafts(app) else {
    334       return XCTFail("The Drafts sheet did not open for retry")
    335     }
    336     let unverified = app.descendants(matching: .any).matching(
    337       NSPredicate(format: "label CONTAINS '0 of 1 photos verified'")
    338     ).firstMatch
    339     XCTAssertTrue(unverified.waitForExistence(timeout: 20))
    340     let reopen = app.buttons["Reopen"].firstMatch
    341     XCTAssertTrue(reopen.waitForExistence(timeout: 10))
    342     reopen.tap()
    343 
    344     guard let submit = readySubmit(app),
    345       let terminalSubmitValue = submitAndWait(app, submit: submit)
    346     else { return }
    347     guard !terminalSubmitValue.contains("Error code") else {
    348       return XCTFail("The persisted Blossom retry failed; submit.value=\(terminalSubmitValue)")
    349     }
    350     guard openDrafts(app) else {
    351       return XCTFail("The Drafts sheet did not open after retry")
    352     }
    353     XCTAssertTrue(
    354       app.descendants(matching: .any).matching(
    355         NSPredicate(format: "label CONTAINS '1 of 1 photos verified'")
    356       ).firstMatch.waitForExistence(timeout: 20)
    357     )
    358     app.buttons["Done"].tap()
    359 
    360     app.terminate()
    361     app.launch()
    362     reachRoot(app)
    363     XCTAssertTrue(app.tabBars.firstMatch.waitForExistence(timeout: 20))
    364   }
    365 
    366   @MainActor
    367   func testLocalCancellationPersistsAfterVerifiedUpload() throws {
    368     let configuration = try QualificationConfiguration.environment()
    369     let app = launchToRoot(configuration)
    370     guard openAdd(app) != nil else {
    371       return XCTFail("The Add bottom tab did not present the Add surface")
    372     }
    373     guard openDrafts(app) else {
    374       return XCTFail("The Drafts sheet did not open for cancellation")
    375     }
    376 
    377     let cancel = app.buttons["Cancel"].firstMatch
    378     XCTAssertTrue(cancel.waitForExistence(timeout: 20))
    379     cancel.tap()
    380     let cancelled = app.staticTexts.matching(
    381       NSPredicate(format: "label == 'Cancelled'")
    382     ).firstMatch
    383     XCTAssertTrue(cancelled.waitForExistence(timeout: 20))
    384     app.buttons["Done"].tap()
    385 
    386     app.terminate()
    387     app.launch()
    388     reachRoot(app)
    389     guard openAdd(app) != nil else {
    390       return XCTFail("The Add bottom tab did not recover after cancellation")
    391     }
    392     guard openDrafts(app) else {
    393       return XCTFail("The Drafts sheet did not reopen after cancellation")
    394     }
    395     let persistedCancelled = app.staticTexts.matching(
    396       NSPredicate(format: "label == 'Cancelled'")
    397     ).firstMatch
    398     XCTAssertTrue(persistedCancelled.waitForExistence(timeout: 20))
    399     app.buttons["Done"].tap()
    400   }
    401 
    402   @MainActor
    403   func testManualDeviceLockAndProtectedDataRecovery() throws {
    404     let configuration = try QualificationConfiguration.environment()
    405     let app = launchToRoot(configuration)
    406     XCTAssertTrue(app.tabBars.firstMatch.waitForExistence(timeout: 20))
    407 
    408     XCUIDevice.shared.press(.home)
    409     Thread.sleep(forTimeInterval: 60)
    410 
    411     for _ in 0 ..< 24 {
    412       app.activate()
    413       if app.tabBars.firstMatch.waitForExistence(timeout: 5) {
    414         return
    415       }
    416     }
    417     XCTFail("Tera did not recover after the physical device was unlocked")
    418   }
    419 
    420   @MainActor
    421   private func launchToRoot(
    422     _ configuration: QualificationConfiguration,
    423     launchArguments: [String] = []
    424   ) -> XCUIApplication {
    425     let app = XCUIApplication()
    426     app.launchEnvironment = configuration.launchEnvironment
    427     app.launchArguments = launchArguments
    428     app.launch()
    429     reachRoot(app)
    430     return app
    431   }
    432 
    433   @MainActor
    434   private func launchPersona(_ configuration: QualificationConfiguration) -> XCUIApplication {
    435     launchToRoot(
    436       configuration,
    437       launchArguments: [
    438         "-AppleLanguages", "(en)",
    439         "-AppleLocale", "en_US",
    440         "-UIAccessibilityReduceMotionEnabled", "YES",
    441       ]
    442     )
    443   }
    444 
    445   @MainActor
    446   private func performLocalSocialAccessibilityAudit(
    447     _ app: XCUIApplication,
    448     includeContrast: Bool = true
    449   ) throws {
    450     let findings = try TeraAccessibilityAudit(test: self).run(app, includeContrast: includeContrast)
    451     XCTAssertTrue(findings.isEmpty, "Unresolved accessibility findings:\n" + findings.joined(separator: "\n"))
    452   }
    453 
    454   @MainActor
    455   private func assertProgressiveDisclosure(_ app: XCUIApplication, type: String) {
    456     scrollAddFormToTop(app)
    457     defer { scrollAddFormToTop(app) }
    458     let expected = expectedAddFields[type] ?? []
    459     for identifier in allAddFields {
    460       let element = app.descendants(matching: .any)[identifier]
    461       if expected.contains(identifier) {
    462         scrollTo(app, element: element)
    463         XCTAssertTrue(
    464           element.waitForExistence(timeout: 10),
    465           "Missing progressive-disclosure field for \(type): \(identifier)"
    466         )
    467         XCTAssertEqual(
    468           element.label,
    469           expectedAddFieldLabel(identifier, type: type),
    470           "Unexpected accessibility label: \(identifier)"
    471         )
    472       } else {
    473         XCTAssertFalse(
    474           element.exists,
    475           "Unexpected progressive-disclosure field for \(type): \(identifier)"
    476         )
    477       }
    478     }
    479     let expectsMedia = type != "Update"
    480     let mediaLibrary = app.buttons["radroots.add.media.library"]
    481     let mediaCamera = app.buttons["radroots.add.media.camera"]
    482     if expectsMedia {
    483       scrollTo(app, element: mediaLibrary)
    484       XCTAssertTrue(mediaLibrary.waitForExistence(timeout: 10))
    485       XCTAssertTrue(mediaCamera.waitForExistence(timeout: 10))
    486       XCTAssertEqual(mediaLibrary.label, "Photo Library")
    487       XCTAssertEqual(mediaCamera.label, "Camera")
    488     } else {
    489       XCTAssertFalse(mediaLibrary.exists, "Unexpected media-library disclosure for \(type)")
    490       XCTAssertFalse(mediaCamera.exists, "Unexpected camera disclosure for \(type)")
    491     }
    492   }
    493 
    494   private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {
    495     switch identifier {
    496     case "radroots.add.content":
    497       switch type {
    498       case "Update":
    499         "What’s happening locally?"
    500       case "Photo update":
    501         "What should neighbors know?"
    502       case "Ask":
    503         "What do you need or want to know?"
    504       case "Event":
    505         "Event details (optional)"
    506       case "Food availability":
    507         "Details"
    508       default:
    509         preconditionFailure("Unrecognized Add type: \(type)")
    510       }
    511     case "radroots.add.title":
    512       type == "Food availability" ? "Food" : "Title"
    513     case "radroots.add.summary":
    514       "Short summary"
    515     case "radroots.add.location":
    516       type == "Food availability" ? "Public location" : "Public location (optional)"
    517     case "radroots.add.event_timing":
    518       "When, Specific time"
    519     case "radroots.add.event.start":
    520       "Starts"
    521     case "radroots.add.event.end":
    522       "Ends"
    523     case "radroots.add.price":
    524       "Price"
    525     case "radroots.add.currency":
    526       "Currency"
    527     case "radroots.add.unit":
    528       "Unit, Choose a unit"
    529     case "radroots.add.quantity":
    530       "Quantity available (optional)"
    531     default:
    532       preconditionFailure("Unrecognized Add field identifier: \(identifier)")
    533     }
    534   }
    535 
    536   private var allAddFields: [String] {
    537     [
    538       "radroots.add.content",
    539       "radroots.add.title",
    540       "radroots.add.summary",
    541       "radroots.add.location",
    542       "radroots.add.event_timing",
    543       "radroots.add.event.start",
    544       "radroots.add.event.end",
    545       "radroots.add.price",
    546       "radroots.add.currency",
    547       "radroots.add.unit",
    548       "radroots.add.quantity",
    549     ]
    550   }
    551 
    552   private var expectedAddFields: [String: Set<String>] {
    553     [
    554       "Update": ["radroots.add.content"],
    555       "Photo update": ["radroots.add.content"],
    556       "Ask": ["radroots.add.content"],
    557       "Event": [
    558         "radroots.add.content",
    559         "radroots.add.title",
    560         "radroots.add.location",
    561         "radroots.add.event_timing",
    562         "radroots.add.event.start",
    563         "radroots.add.event.end",
    564       ],
    565       "Food availability": [
    566         "radroots.add.content",
    567         "radroots.add.title",
    568         "radroots.add.summary",
    569         "radroots.add.location",
    570         "radroots.add.price",
    571         "radroots.add.currency",
    572         "radroots.add.unit",
    573         "radroots.add.quantity",
    574       ],
    575     ]
    576   }
    577 
    578   @MainActor
    579   private func reachRoot(_ app: XCUIApplication) {
    580     for _ in 0 ..< 6 {
    581       if app.tabBars.firstMatch.waitForExistence(timeout: 3) {
    582         return
    583       }
    584       for identifier in [
    585         "radroots.identity.create",
    586         "radroots.identity.unlock",
    587         "radroots.configuration.reconfigure",
    588         "radroots.identity.recover",
    589         "radroots.runtime.retry",
    590       ] {
    591         let action = app.descendants(matching: .any)[identifier]
    592         if action.exists, action.isHittable {
    593           action.tap()
    594           break
    595         }
    596       }
    597     }
    598     XCTFail("Tera did not reach the two-tab root without interactive authentication")
    599   }
    600 
    601   @MainActor
    602   private func readPublicKey(_ app: XCUIApplication) throws -> String {
    603     app.tabBars.buttons["Today"].tap()
    604     let account = app.descendants(matching: .any)["radroots.support.account"]
    605     XCTAssertTrue(account.waitForExistence(timeout: 10))
    606     account.tap()
    607 
    608     let meSheet = app.descendants(matching: .any)["radroots.support.me.sheet"]
    609     guard meSheet.waitForExistence(timeout: 10) else {
    610       throw QualificationError.missingProductSurface
    611     }
    612     let publicKey = app.descendants(matching: .any)[
    613       "radroots.settings.identity.public_key"
    614     ]
    615     guard openSettingsFromMe(app, meSheet: meSheet) else {
    616       XCTFail("Settings was unavailable through the visible Me sheet")
    617       throw QualificationError.missingProductSurface
    618     }
    619     guard publicKey.waitForExistence(timeout: 10) else {
    620       XCTFail("Settings did not present the native identity public key")
    621       throw QualificationError.missingProductSurface
    622     }
    623     guard let value = publicKey.value as? String,
    624       value.range(of: "^[0-9a-f]{64}$", options: .regularExpression) != nil
    625     else {
    626       throw QualificationError.invalidPublicKey
    627     }
    628     let back = app.navigationBars.buttons["Me"].firstMatch
    629     XCTAssertTrue(back.waitForExistence(timeout: 10))
    630     back.tap()
    631     let done = app.navigationBars.buttons["Done"].firstMatch
    632     XCTAssertTrue(done.waitForExistence(timeout: 10))
    633     done.tap()
    634     XCTAssertTrue(
    635       app.descendants(matching: .any)["radroots.support.me.sheet"]
    636         .waitForNonExistence(timeout: 10)
    637     )
    638     return value
    639   }
    640 
    641   @MainActor
    642   private func readBlossomFailureEvidence(_ app: XCUIApplication) -> String {
    643     app.tabBars.buttons["Today"].tap()
    644     let account = app.descendants(matching: .any)["radroots.support.account"]
    645     guard account.waitForExistence(timeout: 10) else { return "settings=unavailable" }
    646     account.tap()
    647 
    648     let meSheet = app.descendants(matching: .any)["radroots.support.me.sheet"]
    649     guard meSheet.waitForExistence(timeout: 10),
    650       openSettingsFromMe(app, meSheet: meSheet)
    651     else { return "settings=unavailable" }
    652 
    653     let httpStatus = app.descendants(matching: .any)[
    654       "radroots.settings.blossom.http_status"
    655     ]
    656     let errorCode = app.descendants(matching: .any)[
    657       "radroots.settings.blossom.error_code"
    658     ]
    659     let serverErrorCode = app.descendants(matching: .any)[
    660       "radroots.settings.blossom.server_error_code"
    661     ]
    662     for _ in 0 ..< 6 where !httpStatus.exists || !errorCode.exists || !serverErrorCode.exists {
    663       app.swipeUp()
    664     }
    665     _ = httpStatus.waitForExistence(timeout: 10)
    666     _ = errorCode.waitForExistence(timeout: 10)
    667     _ = serverErrorCode.waitForExistence(timeout: 10)
    668     return "http=\(httpStatus.exists ? httpStatus.label : "missing"), "
    669       + "error=\(errorCode.exists ? errorCode.label : "missing"), "
    670       + "server=\(serverErrorCode.exists ? serverErrorCode.label : "missing")"
    671   }
    672 
    673   @MainActor
    674   private func publishTextFlow(
    675     _ app: XCUIApplication,
    676     type: String,
    677     marker: String
    678   ) throws {
    679     try beginDraft(app, type: type)
    680     try enterText(app, identifier: "radroots.add.content", value: marker)
    681     try submitSuccessfully(app)
    682   }
    683 
    684   @MainActor
    685   private func publishEvent(_ app: XCUIApplication, marker: String) throws {
    686     try beginDraft(app, type: "Event")
    687     try enterText(app, identifier: "radroots.add.title", value: marker)
    688     try submitSuccessfully(app)
    689   }
    690 
    691   @MainActor
    692   private func publishFood(_ app: XCUIApplication, marker: String) throws {
    693     try beginDraft(app, type: "Food availability")
    694     try enterText(app, identifier: "radroots.add.title", value: marker)
    695     try enterText(app, identifier: "radroots.add.summary", value: "Fresh local food")
    696     try enterText(app, identifier: "radroots.add.content", value: "Available today")
    697     try enterText(app, identifier: "radroots.add.location", value: "Town square")
    698     try enterText(app, identifier: "radroots.add.price", value: "3")
    699     try enterText(app, identifier: "radroots.add.currency", value: "CAD")
    700     let unit = app.descendants(matching: .any)["radroots.add.unit"]
    701     scrollTo(app, element: unit)
    702     XCTAssertTrue(unit.waitForExistence(timeout: 10))
    703     unit.tap()
    704     let pounds = app.buttons["lb"]
    705     XCTAssertTrue(pounds.waitForExistence(timeout: 10))
    706     pounds.tap()
    707     try submitSuccessfully(app)
    708   }
    709 
    710   @MainActor
    711   private func publishPersonaAttempt(
    712     _ app: XCUIApplication,
    713     attempt: PersonaAttempt,
    714     interactionProfile: String
    715   ) throws -> PersonaInteractionObservation {
    716     let type = attempt.flow.uiLabel
    717     try beginDraft(app, type: type)
    718     var progressiveDisclosure = false
    719     var accessibilitySemantics = false
    720     if interactionProfile == "novice_progressive_disclosure" {
    721       assertProgressiveDisclosure(app, type: type)
    722       progressiveDisclosure = true
    723     }
    724     if interactionProfile == "novice_accessibility_keyboard" {
    725       // The dedicated accessibility test owns the exact full contrast lane.
    726       // Persona attempts retain every semantic audit plus keyboard/focus use.
    727       try performLocalSocialAccessibilityAudit(app, includeContrast: false)
    728       accessibilitySemantics = true
    729     }
    730     try completeOpenDraft(app, flow: attempt.flow, marker: attempt.marker)
    731     return PersonaInteractionObservation(
    732       validationAttempted: false,
    733       validationRejected: false,
    734       retryAttempts: 0,
    735       relaunches: 0,
    736       progressiveDisclosure: progressiveDisclosure,
    737       accessibilitySemantics: accessibilitySemantics,
    738       keyboardFocus: accessibilitySemantics,
    739       visibleActions: true
    740     )
    741   }
    742 
    743   @MainActor
    744   private func completeOpenDraft(
    745     _ app: XCUIApplication,
    746     flow: PersonaFlow,
    747     marker: String
    748   ) throws {
    749     switch flow {
    750     case .update, .ask:
    751       try enterText(app, identifier: "radroots.add.content", value: marker)
    752     case .photoUpdate:
    753       try enterText(app, identifier: "radroots.add.content", value: marker)
    754       let library = app.descendants(matching: .any)["radroots.add.media.library"]
    755       scrollTo(app, element: library)
    756       XCTAssertTrue(library.waitForExistence(timeout: 10))
    757       XCTAssertTrue(library.isEnabled)
    758       library.tap()
    759       let prepared = app.descendants(matching: .any)["radroots.add.media.prepared"]
    760       scrollTo(app, element: prepared)
    761       guard prepared.waitForExistence(timeout: 60) else {
    762         XCTFail("The governed prepared Photo update was unavailable through the visible UI")
    763         throw QualificationError.missingProductSurface
    764       }
    765       guard prepared.isHittable else {
    766         XCTFail("The governed prepared Photo update was obscured")
    767         throw QualificationError.missingProductSurface
    768       }
    769       try enterPhotoDescription(app)
    770     case .event:
    771       try enterText(app, identifier: "radroots.add.title", value: marker)
    772     case .foodAvailability:
    773       try enterText(app, identifier: "radroots.add.title", value: marker)
    774       try enterText(app, identifier: "radroots.add.summary", value: "Fresh local food")
    775       try enterText(app, identifier: "radroots.add.content", value: "Available today")
    776       try enterText(app, identifier: "radroots.add.location", value: "Town square")
    777       try enterText(app, identifier: "radroots.add.price", value: "3")
    778       try enterText(app, identifier: "radroots.add.currency", value: "CAD")
    779       let unit = app.descendants(matching: .any)["radroots.add.unit"]
    780       scrollTo(app, element: unit)
    781       XCTAssertTrue(unit.waitForExistence(timeout: 10))
    782       unit.tap()
    783       let pounds = app.buttons["lb"]
    784       XCTAssertTrue(pounds.waitForExistence(timeout: 10))
    785       pounds.tap()
    786     }
    787     try submitSuccessfully(app)
    788   }
    789 
    790   @MainActor
    791   private func publishWithValidationRecovery(
    792     _ app: XCUIApplication,
    793     attempt: PersonaAttempt
    794   ) throws -> PersonaInteractionObservation {
    795     guard attempt.flow == .event else {
    796       throw QualificationError.invalidPersonaFixture
    797     }
    798     try beginDraft(app, type: attempt.flow.uiLabel)
    799     try enterText(app, identifier: "radroots.add.content", value: attempt.marker)
    800     guard let submit = readySubmit(app), let value = submitAndWait(app, submit: submit) else {
    801       throw QualificationError.missingProductSurface
    802     }
    803     XCTAssertTrue(value.contains("Error code"))
    804     let content = app.descendants(matching: .any)["radroots.add.content"]
    805     scrollTo(app, element: content)
    806     XCTAssertEqual(content.value as? String, attempt.marker)
    807     try enterText(app, identifier: "radroots.add.title", value: attempt.marker)
    808     try submitSuccessfully(app)
    809     return PersonaInteractionObservation(
    810       validationAttempted: true,
    811       validationRejected: true,
    812       retryAttempts: 0,
    813       relaunches: 0,
    814       progressiveDisclosure: false,
    815       accessibilitySemantics: false,
    816       keyboardFocus: false,
    817       visibleActions: true
    818     )
    819   }
    820 
    821   @MainActor
    822   private func publishWithTransportRetry(
    823     _ app: XCUIApplication,
    824     configuration: QualificationConfiguration,
    825     attempt: PersonaAttempt
    826   ) throws -> (app: XCUIApplication, observation: PersonaInteractionObservation) {
    827     guard attempt.flow == .ask else {
    828       throw QualificationError.invalidPersonaFixture
    829     }
    830     try beginDraft(app, type: attempt.flow.uiLabel)
    831     try enterText(app, identifier: "radroots.add.content", value: attempt.marker)
    832     guard let submit = readySubmit(app), submitAndWait(app, submit: submit) != nil else {
    833       throw QualificationError.missingProductSurface
    834     }
    835     app.terminate()
    836 
    837     let relaunched = launchPersona(configuration)
    838     guard openAdd(relaunched) != nil, openDrafts(relaunched) else {
    839       throw QualificationError.missingProductSurface
    840     }
    841     let retry = relaunched.buttons["Retry"].firstMatch
    842     guard retry.waitForExistence(timeout: 20) else {
    843       XCTFail("The transport-retry draft did not expose the Retry action")
    844       throw QualificationError.missingProductSurface
    845     }
    846     retry.tap()
    847     let retryCompleted = NSPredicate { _, _ in !retry.exists || !retry.isEnabled }
    848     let expectation = XCTNSPredicateExpectation(predicate: retryCompleted, object: relaunched)
    849     XCTAssertEqual(XCTWaiter.wait(for: [expectation], timeout: 180), .completed)
    850     let done = relaunched.buttons["Done"]
    851     XCTAssertTrue(done.waitForExistence(timeout: 10))
    852     done.tap()
    853     return (
    854       relaunched,
    855       PersonaInteractionObservation(
    856         validationAttempted: false,
    857         validationRejected: false,
    858         retryAttempts: 1,
    859         relaunches: 1,
    860         progressiveDisclosure: false,
    861         accessibilitySemantics: false,
    862         keyboardFocus: false,
    863         visibleActions: true
    864       )
    865     )
    866   }
    867 
    868   private func loadPersonaSuite() throws -> PersonaSuite {
    869     let bundle = Bundle(for: TeraRemoteQualificationUITests.self)
    870     let url = try XCTUnwrap(
    871       bundle.url(forResource: "local-social-personas.v1", withExtension: "json")
    872     )
    873     let data = try Data(contentsOf: url, options: [.mappedIfSafe])
    874     guard data.count <= 64 * 1024 else {
    875       throw QualificationError.invalidPersonaFixture
    876     }
    877     return try JSONDecoder().decode(PersonaSuite.self, from: data)
    878   }
    879 
    880   private func activateFixture(persona: String, at path: String) throws {
    881     guard ["P01", "P02", "P03", "P04", "P05"].contains(persona) else {
    882       throw QualificationError.invalidPersonaFixture
    883     }
    884     let data = try JSONSerialization.data(
    885       withJSONObject: [
    886         "schema": "tera.ios.local-social.persona-control.v1",
    887         "active_persona": persona,
    888         "blossom_enabled": true,
    889       ],
    890       options: [.sortedKeys]
    891     )
    892     try data.write(to: URL(fileURLWithPath: path), options: [.atomic])
    893   }
    894 
    895   private func readFixtureNetwork(
    896     _ configuration: QualificationConfiguration
    897   ) throws -> PersonaFixtureNetworkSnapshot {
    898     let path = try XCTUnwrap(configuration.fixtureEvidence)
    899     let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
    900     defer { try? handle.close() }
    901     let data = try handle.read(upToCount: 64 * 1024 + 1) ?? Data()
    902     guard !data.isEmpty, data.count <= 64 * 1024 else {
    903       throw QualificationError.invalidFixtureEvidence
    904     }
    905     let root = try JSONSerialization.jsonObject(with: data)
    906     guard let value = root as? [String: Any] else {
    907       throw QualificationError.invalidFixtureEvidence
    908     }
    909     return try PersonaFixtureNetworkSnapshot(value)
    910   }
    911 
    912   @MainActor
    913   private func beginDraft(_ app: XCUIApplication, type: String) throws {
    914     guard openAdd(app) != nil else {
    915       XCTFail("The Add bottom tab did not present the real Add store")
    916       throw QualificationError.missingProductSurface
    917     }
    918     let newDraft = app.buttons["radroots.add.new"]
    919     guard beginNewDraft(newDraft) else {
    920       XCTFail("The New draft action was unavailable")
    921       throw QualificationError.missingProductSurface
    922     }
    923     // The explicit replacement must settle before changing the new form.
    924     scrollAddFormToTop(app)
    925     let picker = app.descendants(matching: .any)["radroots.add.type"]
    926     scrollTo(app, element: picker)
    927     guard picker.waitForExistence(timeout: 10), waitUntilHittable(picker, timeout: 10) else {
    928       XCTFail("The Add type picker was unavailable")
    929       throw QualificationError.missingProductSurface
    930     }
    931     if picker.value as? String == type {
    932       return
    933     }
    934     picker.tap()
    935     let option = app.buttons[type]
    936     guard option.waitForExistence(timeout: 10), waitUntilHittable(option, timeout: 10) else {
    937       XCTFail("The Add type picker did not present \(type)")
    938       throw QualificationError.missingProductSurface
    939     }
    940     option.tap()
    941     guard waitForValue(picker, value: type, timeout: 10) else {
    942       XCTFail("The Add type picker did not select \(type)")
    943       throw QualificationError.missingProductSurface
    944     }
    945   }
    946 
    947   @MainActor
    948   func enterText(
    949     _ app: XCUIApplication,
    950     identifier: String,
    951     value: String
    952   ) throws {
    953     let field = app.descendants(matching: .any)[identifier]
    954     scrollTo(app, element: field)
    955     guard field.waitForExistence(timeout: 10), waitUntilHittable(field, timeout: 10) else {
    956       XCTFail("The Add field \(identifier) was unavailable")
    957       throw QualificationError.missingProductSurface
    958     }
    959     guard focusForTextInput(field, timeout: 10) else {
    960       XCTFail("The Add field \(identifier) did not accept keyboard focus")
    961       throw QualificationError.missingProductSurface
    962     }
    963     enterExactText(field, value: value)
    964     let done = app.buttons["radroots.add.keyboard.done"]
    965     if done.waitForExistence(timeout: 5) {
    966       done.tap()
    967       XCTAssertFalse(app.keyboards.firstMatch.waitForExistence(timeout: 5))
    968     }
    969   }
    970 
    971   @MainActor
    972   private func submitSuccessfully(_ app: XCUIApplication) throws {
    973     guard let submit = readySubmit(app),
    974       let value = submitAndWait(app, submit: submit)
    975     else {
    976       throw QualificationError.missingProductSurface
    977     }
    978     guard !value.contains("Error code"), finishOriginalPublication(app, submit: submit) else {
    979       XCTFail("The local-social flow failed: \(value)")
    980       throw QualificationError.productSubmissionFailed
    981     }
    982   }
    983 
    984   @MainActor
    985   func scrollTo(_ app: XCUIApplication, element: XCUIElement) {
    986     let root = app.descendants(matching: .any)["radroots.add.root"]
    987     let tabBar = app.tabBars.firstMatch
    988     for attempt in 0 ..< 16 where !isVisibleInAddViewport(
    989       element,
    990       root: root,
    991       above: tabBar
    992     ) {
    993       if element.exists {
    994         let targetFrame = element.frame
    995         let rootFrame = root.frame
    996         if targetFrame.midY < rootFrame.midY {
    997           root.swipeDown()
    998         } else {
    999           root.swipeUp()
   1000         }
   1001       } else if attempt < 8 {
   1002         root.swipeUp()
   1003       } else {
   1004         root.swipeDown()
   1005       }
   1006     }
   1007   }
   1008 
   1009   @MainActor
   1010   private func isVisibleInAddViewport(
   1011     _ element: XCUIElement,
   1012     root: XCUIElement,
   1013     above obstruction: XCUIElement
   1014   ) -> Bool {
   1015     guard element.exists, element.isHittable, root.exists, obstruction.exists else {
   1016       return false
   1017     }
   1018     let frame = element.frame
   1019     let rootFrame = root.frame
   1020     return frame.height > 0
   1021       && frame.minY >= rootFrame.minY
   1022       && frame.maxY <= min(rootFrame.maxY, obstruction.frame.minY)
   1023   }
   1024 
   1025   @MainActor
   1026   private func scrollAddFormToTop(_ app: XCUIApplication) {
   1027     let root = app.descendants(matching: .any)["radroots.add.root"]
   1028     for _ in 0 ..< 8 {
   1029       root.swipeDown()
   1030     }
   1031   }
   1032 
   1033   @MainActor
   1034   private func assertTodayContains(_ app: XCUIApplication, markers: [String]) throws {
   1035     app.tabBars.buttons["Today"].tap()
   1036     let refresh = app.buttons["radroots.today.refresh"]
   1037     guard refresh.waitForExistence(timeout: 10), refresh.isHittable else {
   1038       XCTFail("The ordinary Today refresh action was unavailable")
   1039       throw QualificationError.missingProductSurface
   1040     }
   1041     refresh.tap()
   1042     let feed = app.descendants(matching: .any)["radroots.today.feed"].firstMatch
   1043     guard feed.waitForExistence(timeout: 30) else {
   1044       XCTFail("The local relay refresh did not materialize the Today feed")
   1045       throw QualificationError.missingProductSurface
   1046     }
   1047     for marker in markers.reversed() {
   1048       for _ in 0 ..< 8 {
   1049         feed.swipeDown()
   1050       }
   1051       let card = app.descendants(matching: .any).matching(
   1052         NSPredicate(format: "label CONTAINS %@", marker)
   1053       ).firstMatch
   1054       for _ in 0 ..< 6 where !card.exists {
   1055         feed.swipeUp()
   1056       }
   1057       guard card.waitForExistence(timeout: 30) else {
   1058         XCTFail("Missing Today card \(marker)")
   1059         throw QualificationError.missingProductSurface
   1060       }
   1061     }
   1062   }
   1063 
   1064   @MainActor
   1065   func readySubmit(_ app: XCUIApplication) -> XCUIElement? {
   1066     let submit = app.descendants(matching: .any)["radroots.add.submit"]
   1067     let addRoot = app.descendants(matching: .any)["radroots.add.root"]
   1068     for _ in 0 ..< 8 {
   1069       if isUnobscured(submit, above: app.tabBars.firstMatch) {
   1070         break
   1071       }
   1072       addRoot.swipeUp()
   1073     }
   1074     guard submit.waitForExistence(timeout: 10),
   1075       submit.isEnabled,
   1076       waitUntilHittable(submit, timeout: 10),
   1077       isUnobscured(submit, above: app.tabBars.firstMatch)
   1078     else {
   1079       XCTFail(
   1080         "The Add submission control did not become unobscured; "
   1081           + submissionDiagnostics(app, submit: submit)
   1082       )
   1083       return nil
   1084     }
   1085     return submit
   1086   }
   1087 
   1088   @MainActor
   1089   func submitAndWait(_ app: XCUIApplication, submit: XCUIElement) -> String? {
   1090     let status = app.staticTexts.matching(identifier: "radroots.add.status").firstMatch
   1091     let priorStatusLabel = status.exists ? status.label : nil
   1092     let priorSubmitValue = submit.value as? String
   1093     submit.tap()
   1094     guard
   1095       waitForWorkToFinish(
   1096         app,
   1097         submit: submit,
   1098         status: status,
   1099         priorStatusLabel: priorStatusLabel,
   1100         priorSubmitValue: priorSubmitValue
   1101       )
   1102     else {
   1103       XCTFail(
   1104         "The Add submission did not reach a terminal local state; "
   1105           + submissionDiagnostics(app, submit: submit)
   1106       )
   1107       return nil
   1108     }
   1109     scrollTo(app, element: submit)
   1110     guard submit.waitForExistence(timeout: 10), terminalControlShowsSettledState(submit) else {
   1111       XCTFail("The terminal Add submission control was unavailable")
   1112       return nil
   1113     }
   1114     let terminalValue = submit.value as? String ?? "missing"
   1115     let statusChanged = status.exists
   1116       && !status.label.isEmpty
   1117       && status.label != priorStatusLabel
   1118     guard statusChanged || terminalValue != priorSubmitValue && terminalValue != "Working" else {
   1119       XCTFail(
   1120         "The Add submission did not expose changed terminal evidence; "
   1121           + submissionDiagnostics(app, submit: submit)
   1122       )
   1123       return nil
   1124     }
   1125     return terminalValue
   1126   }
   1127 
   1128   @MainActor
   1129   private func assertUnverifiedDraft(_ app: XCUIApplication) -> Bool {
   1130     guard openDrafts(app) else {
   1131       XCTFail("The Drafts sheet did not open after the unavailable attempt")
   1132       return false
   1133     }
   1134     let selectedMedia = app.descendants(matching: .any)["tera.add.submission.media_status"]
   1135     let viewSubmission = app.buttons["View submission"].firstMatch
   1136     if !selectedMedia.exists, viewSubmission.exists {
   1137       viewSubmission.tap()
   1138     }
   1139     let mediaStatus = app.descendants(matching: .any).matching(
   1140       NSPredicate(format: "label CONTAINS '0 of 1 photos verified'")
   1141     ).firstMatch
   1142     let exists = mediaStatus.waitForExistence(timeout: 20)
   1143     XCTAssertTrue(exists)
   1144     if exists {
   1145       XCTAssertTrue(
   1146         mediaStatus.label == "0 of 1 photos verified"
   1147           || mediaStatus.label == "0 of 1 photos verified; 1 possible orphan",
   1148         "An unavailable upload did not preserve a bounded retry state; "
   1149           + "media_status.label=\(mediaStatus.label)"
   1150       )
   1151     }
   1152     app.buttons["Done"].tap()
   1153     return exists
   1154   }
   1155 
   1156   @MainActor
   1157   private func isUnobscured(_ element: XCUIElement, above obstruction: XCUIElement) -> Bool {
   1158     guard element.exists, element.isEnabled, element.isHittable, obstruction.exists else {
   1159       return false
   1160     }
   1161     let frame = element.frame
   1162     return frame.height > 0
   1163       && frame.minY >= 0
   1164       && frame.maxY <= obstruction.frame.minY
   1165   }
   1166 
   1167   @MainActor
   1168   private func submissionDiagnostics(
   1169     _ app: XCUIApplication,
   1170     submit: XCUIElement
   1171   ) -> String {
   1172     let progress = app.descendants(matching: .any)["radroots.add.progress"]
   1173     let status = app.staticTexts.matching(identifier: "radroots.add.status").firstMatch
   1174     let statusLabel = status.exists ? status.label : "missing"
   1175     guard submit.exists else {
   1176       return "submit.exists=false, progress.exists=\(progress.exists), status=\(statusLabel)"
   1177     }
   1178     let tabBar = app.tabBars.firstMatch
   1179     return "submit.exists=\(submit.exists), submit.enabled=\(submit.isEnabled), "
   1180       + "submit.hittable=\(submit.isHittable), submit.value=\(String(describing: submit.value)), "
   1181       + "submit.frame=\(submit.frame), "
   1182       + "tab_bar_top=\(tabBar.exists ? tabBar.frame.minY : -1), "
   1183       + "progress.exists=\(progress.exists), status=\(statusLabel)"
   1184   }
   1185 
   1186   @MainActor
   1187   func openDrafts(_ app: XCUIApplication) -> Bool {
   1188     let drafts = app.buttons["radroots.add.drafts"]
   1189     guard drafts.waitForExistence(timeout: 10) else { return false }
   1190     let sheet = app.descendants(matching: .any)["radroots.add.drafts.sheet"]
   1191     for _ in 0 ..< 3 {
   1192       drafts.tap()
   1193       if sheet.waitForExistence(timeout: 10) {
   1194         return true
   1195       }
   1196     }
   1197     return false
   1198   }
   1199 
   1200   @MainActor
   1201   func waitUntilHittable(_ element: XCUIElement, timeout: TimeInterval) -> Bool {
   1202     let predicate = NSPredicate(format: "exists == true AND hittable == true")
   1203     let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
   1204     return XCTWaiter.wait(for: [expectation], timeout: timeout) == .completed
   1205   }
   1206 
   1207   @MainActor
   1208   private func openSettingsFromMe(
   1209     _ app: XCUIApplication,
   1210     meSheet: XCUIElement
   1211   ) -> Bool {
   1212     let meList = meSheet.descendants(matching: .collectionView).firstMatch
   1213     guard meList.waitForExistence(timeout: 10) else { return false }
   1214     let settings = app.descendants(matching: .any)["radroots.support.settings"]
   1215     for _ in 0 ..< 8 {
   1216       if settings.exists,
   1217         settings.isHittable,
   1218         settings.frame.minY >= meSheet.frame.minY,
   1219         settings.frame.maxY <= meSheet.frame.maxY
   1220       {
   1221         settings.tap()
   1222         return app.descendants(matching: .any)["radroots.support.settings.view"]
   1223           .waitForExistence(timeout: 20)
   1224       }
   1225       meList.swipeUp()
   1226     }
   1227     return false
   1228   }
   1229 
   1230   @MainActor
   1231   private func focusForTextInput(_ element: XCUIElement, timeout: TimeInterval) -> Bool {
   1232     let focused = NSPredicate(format: "hasKeyboardFocus == true")
   1233     for _ in 0 ..< 3 {
   1234       element.tap()
   1235       let expectation = XCTNSPredicateExpectation(predicate: focused, object: element)
   1236       if XCTWaiter.wait(for: [expectation], timeout: timeout / 3) == .completed {
   1237         return true
   1238       }
   1239     }
   1240     return false
   1241   }
   1242 
   1243   @MainActor
   1244   func waitForValue(
   1245     _ element: XCUIElement,
   1246     value: String,
   1247     timeout: TimeInterval
   1248   ) -> Bool {
   1249     let predicate = NSPredicate { object, _ in
   1250       guard let object = object as? XCUIElement else { return false }
   1251       return object.exists && object.value as? String == value
   1252     }
   1253     let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
   1254     return XCTWaiter.wait(for: [expectation], timeout: timeout) == .completed
   1255   }
   1256 
   1257   @MainActor
   1258   private func waitForLabel(
   1259     _ element: XCUIElement,
   1260     label: String,
   1261     timeout: TimeInterval
   1262   ) -> Bool {
   1263     let predicate = NSPredicate { object, _ in
   1264       guard let object = object as? XCUIElement else { return false }
   1265       return object.exists && object.label == label
   1266     }
   1267     let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
   1268     return XCTWaiter.wait(for: [expectation], timeout: timeout) == .completed
   1269   }
   1270 
   1271   private func writeBootstrapReceipt(
   1272     configuration: QualificationConfiguration,
   1273     publicKey: String
   1274   ) throws {
   1275     let receipt = BootstrapReceipt(
   1276       schema: "tera-ios-remote-qualification-bootstrap-v1",
   1277       schemaVersion: 1,
   1278       runID: configuration.runID,
   1279       publicKey: publicKey,
   1280       relayURLs: configuration.relayURLs,
   1281       blossomOrigins: configuration.blossomOrigins,
   1282       privateKeyPresent: false,
   1283       interactiveAuthenticationRequired: false
   1284     )
   1285     let encoder = JSONEncoder()
   1286     encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
   1287     let data = try encoder.encode(receipt)
   1288     let documents = try XCTUnwrap(
   1289       FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
   1290     )
   1291     try data.write(
   1292       to: documents.appendingPathComponent(Self.receiptName),
   1293       options: .atomic
   1294     )
   1295     let attachment = XCTAttachment(data: data, uniformTypeIdentifier: "public.json")
   1296     attachment.name = Self.receiptName
   1297     attachment.lifetime = .keepAlways
   1298     add(attachment)
   1299   }
   1300 
   1301   private func writePersonaAttemptAttachment(
   1302     configuration: QualificationConfiguration,
   1303     personaAlias: String,
   1304     attempt: PersonaAttempt,
   1305     identityDigest: String,
   1306     observation: PersonaAttemptObservation
   1307   ) throws {
   1308     let binding = try XCTUnwrap(configuration.evidenceBinding)
   1309     let evidence = PersonaAttemptEvidence(
   1310       schema: "tera.ios.local-social.persona-attempt-evidence.v1",
   1311       schemaVersion: 1,
   1312       testInvocation: .init(
   1313         target: "TeraUITests",
   1314         identifier: "TeraUITests/testLocalSocialDeterministicPersonas",
   1315         action: "test",
   1316         configuration: "Debug"
   1317       ),
   1318       source: .init(commit: binding.sourceCommit, tree: binding.sourceTree),
   1319       appBuildSHA256: binding.appBuildSHA256,
   1320       simulator: .init(
   1321         udid: binding.simulatorID,
   1322         os: Self.currentIOSVersion,
   1323         architecture: "arm64"
   1324       ),
   1325       runID: configuration.qualificationRunID,
   1326       personaRunID: configuration.runID,
   1327       personaAlias: personaAlias,
   1328       attemptID: attempt.id,
   1329       attemptOrder: attempt.order,
   1330       flow: attempt.flow.rawValue,
   1331       expectedFailure: attempt.expectedFailure,
   1332       publicIdentitySHA256: identityDigest,
   1333       endpointPolicySHA256: Self.endpointPolicyDigest(configuration),
   1334       uiObservation: observation.ui,
   1335       networkObservation: observation.network,
   1336       accessibility: .init(
   1337         locale: "en_US",
   1338         contentSize: "accessibility-extra-extra-extra-large",
   1339         reduceMotion: true,
   1340         progressiveDisclosure: observation.interaction.progressiveDisclosure,
   1341         labelsValuesTraits: observation.interaction.accessibilitySemantics,
   1342         keyboardFocus: observation.interaction.keyboardFocus,
   1343         visibleActions: observation.interaction.visibleActions,
   1344         voiceoverUserObserved: false
   1345       ),
   1346       artifactDigests: []
   1347     )
   1348     let encoder = JSONEncoder()
   1349     encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
   1350     let data = try encoder.encode(evidence)
   1351     XCTAssertLessThanOrEqual(data.count, 64 * 1024)
   1352     let attachment = XCTAttachment(data: data, uniformTypeIdentifier: "public.json")
   1353     attachment.name = "radroots-local-social-\(attempt.id).json"
   1354     attachment.lifetime = .keepAlways
   1355     add(attachment)
   1356   }
   1357 
   1358   private static var currentIOSVersion: String {
   1359     let version = ProcessInfo.processInfo.operatingSystemVersion
   1360     return "iOS \(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
   1361   }
   1362 
   1363   private static func endpointPolicyDigest(
   1364     _ configuration: QualificationConfiguration
   1365   ) -> String {
   1366     var payload = Data("radroots.ios.local-social.endpoint-policy.v1\0".utf8)
   1367     for value in (configuration.relayURLs + configuration.blossomOrigins).sorted() {
   1368       let bytes = Data(value.utf8)
   1369       var length = UInt64(bytes.count).bigEndian
   1370       withUnsafeBytes(of: &length) { payload.append(contentsOf: $0) }
   1371       payload.append(bytes)
   1372     }
   1373     return SHA256.hash(data: payload).map { String(format: "%02x", $0) }.joined()
   1374   }
   1375 }
   1376 
   1377 private struct QualificationEndpointPolicy {
   1378   private enum Role {
   1379     case relay
   1380     case blossom
   1381   }
   1382 
   1383   init(relayURLs: [String], blossomOrigins: [String], profile: String) throws {
   1384     guard !relayURLs.isEmpty,
   1385       Set(relayURLs).count == relayURLs.count,
   1386       blossomOrigins.count == 1
   1387     else {
   1388       throw QualificationError.invalidEndpointPolicy
   1389     }
   1390     if profile == "simulator" {
   1391       guard
   1392         relayURLs.allSatisfy({ Self.isEndpoint($0, role: .relay, loopbackOnly: true) }),
   1393         blossomOrigins.allSatisfy({
   1394           Self.isEndpoint($0, role: .blossom, loopbackOnly: true)
   1395         })
   1396       else {
   1397         throw QualificationError.invalidEndpointPolicy
   1398       }
   1399     } else {
   1400       guard
   1401         relayURLs.allSatisfy({ Self.isEndpoint($0, role: .relay, loopbackOnly: false) }),
   1402         blossomOrigins.allSatisfy({
   1403           Self.isEndpoint($0, role: .blossom, loopbackOnly: false)
   1404         })
   1405       else {
   1406         throw QualificationError.invalidEndpointPolicy
   1407       }
   1408     }
   1409   }
   1410 
   1411   private static func isEndpoint(
   1412     _ raw: String,
   1413     role: Role,
   1414     loopbackOnly: Bool
   1415   ) -> Bool {
   1416     guard let value = URLComponents(string: raw),
   1417       value.user == nil,
   1418       value.password == nil,
   1419       value.query == nil,
   1420       value.fragment == nil,
   1421       let scheme = value.scheme?.lowercased(),
   1422       let host = value.host?.lowercased(),
   1423       !host.isEmpty
   1424     else { return false }
   1425     let expectedScheme = switch (role, loopbackOnly) {
   1426     case (.relay, true): "ws"
   1427     case (.blossom, true): "http"
   1428     case (.relay, false): "wss"
   1429     case (.blossom, false): "https"
   1430     }
   1431     if loopbackOnly {
   1432       return scheme == expectedScheme
   1433         && host == "127.0.0.1"
   1434         && value.port.map { 1 ... 65535 ~= $0 } == true
   1435         && (value.path.isEmpty || value.path == "/")
   1436     }
   1437     return scheme == expectedScheme
   1438       && host != "127.0.0.1"
   1439       && host != "::1"
   1440       && host != "localhost"
   1441   }
   1442 }
   1443 
   1444 private struct QualificationConfiguration {
   1445   static let enabledKey = "TERA_IOS_UI_TEST_REMOTE"
   1446   static let runIDKey = "TERA_IOS_UI_TEST_RUN_ID"
   1447   static let relayURLsKey = "TERA_IOS_UI_TEST_NOSTR_RELAY_URLS"
   1448   static let blossomOriginsKey = "TERA_IOS_UI_TEST_BLOSSOM_ORIGINS"
   1449   static let fixtureControlKey = "TERA_IOS_UI_TEST_FIXTURE_CONTROL"
   1450   static let fixtureEvidenceKey = "TERA_IOS_UI_TEST_FIXTURE_EVIDENCE"
   1451   static let mediaRelativePathKey = "TERA_IOS_UI_TEST_MEDIA_RELATIVE_PATH"
   1452   static let networkProfileKey = "TERA_IOS_UI_TEST_NETWORK_PROFILE"
   1453   static let sourceCommitKey = "TERA_IOS_UI_TEST_SOURCE_COMMIT"
   1454   static let sourceTreeKey = "TERA_IOS_UI_TEST_SOURCE_TREE"
   1455   static let appBuildSHA256Key = "TERA_IOS_UI_TEST_APP_BUILD_SHA256"
   1456   static let simulatorIDKey = "TERA_IOS_UI_TEST_SIMULATOR_ID"
   1457 
   1458   let runID: String
   1459   let qualificationRunID: String
   1460   let relayURLs: [String]
   1461   let blossomOrigins: [String]
   1462   let fixtureControl: String?
   1463   let fixtureEvidence: String?
   1464   let networkProfile: String
   1465   let evidenceBinding: PersonaEvidenceBinding?
   1466 
   1467   init(
   1468     runID: String,
   1469     relayURLs: [String],
   1470     blossomOrigins: [String],
   1471     fixtureControl: String? = nil,
   1472     fixtureEvidence: String? = nil,
   1473     networkProfile: String = "public",
   1474     qualificationRunID: String? = nil,
   1475     evidenceBinding: PersonaEvidenceBinding? = nil
   1476   ) {
   1477     self.runID = runID
   1478     self.qualificationRunID = qualificationRunID ?? runID
   1479     self.relayURLs = relayURLs
   1480     self.blossomOrigins = blossomOrigins
   1481     self.fixtureControl = fixtureControl
   1482     self.fixtureEvidence = fixtureEvidence
   1483     self.networkProfile = networkProfile
   1484     self.evidenceBinding = evidenceBinding
   1485   }
   1486 
   1487   var launchEnvironment: [String: String] {
   1488     [
   1489       Self.enabledKey: "1",
   1490       Self.runIDKey: runID,
   1491       Self.relayURLsKey: relayURLs.joined(separator: ","),
   1492       Self.blossomOriginsKey: blossomOrigins.joined(separator: ","),
   1493       Self.mediaRelativePathKey: "qualification/input.png",
   1494       Self.networkProfileKey: networkProfile,
   1495       "RUST_BACKTRACE": "1",
   1496     ]
   1497   }
   1498 
   1499   func forPersona(_ alias: String) -> Self {
   1500     let digest = SHA256.hash(
   1501       data: Data("radroots.ios.local-social.persona-run.v1\0\(runID)\0\(alias)".utf8)
   1502     )
   1503     let suffix = digest.prefix(12).map { String(format: "%02x", $0) }.joined()
   1504     return Self(
   1505       runID: "persona-\(alias.lowercased())-\(suffix)",
   1506       relayURLs: relayURLs,
   1507       blossomOrigins: blossomOrigins,
   1508       fixtureControl: fixtureControl,
   1509       fixtureEvidence: fixtureEvidence,
   1510       networkProfile: networkProfile,
   1511       qualificationRunID: qualificationRunID,
   1512       evidenceBinding: evidenceBinding
   1513     )
   1514   }
   1515 
   1516   static func environment(
   1517     _ values: [String: String] = ProcessInfo.processInfo.environment
   1518   ) throws -> Self {
   1519     let bundle = Bundle(for: TeraRemoteQualificationUITests.self)
   1520     let runIDValue =
   1521       values[runIDKey]
   1522       ?? bundle.object(forInfoDictionaryKey: runIDKey) as? String
   1523     let relayValue =
   1524       values[relayURLsKey]
   1525       ?? bundle.object(forInfoDictionaryKey: relayURLsKey) as? String
   1526     let blossomValue =
   1527       values[blossomOriginsKey]
   1528       ?? bundle.object(forInfoDictionaryKey: blossomOriginsKey) as? String
   1529     let fixtureControl =
   1530       values[fixtureControlKey]
   1531       ?? bundle.object(forInfoDictionaryKey: fixtureControlKey) as? String
   1532     let fixtureEvidence =
   1533       values[fixtureEvidenceKey]
   1534       ?? bundle.object(forInfoDictionaryKey: fixtureEvidenceKey) as? String
   1535     let networkProfile =
   1536       values[networkProfileKey]
   1537       ?? bundle.object(forInfoDictionaryKey: networkProfileKey) as? String
   1538     let sourceCommit = value(sourceCommitKey, values: values, bundle: bundle)
   1539     let sourceTree = value(sourceTreeKey, values: values, bundle: bundle)
   1540     let appBuildSHA256 = value(appBuildSHA256Key, values: values, bundle: bundle)
   1541     let simulatorID = value(simulatorIDKey, values: values, bundle: bundle)
   1542     if runIDValue?.isEmpty != false, blossomValue?.isEmpty != false {
   1543       throw XCTSkip("remote qualification inputs were not selected")
   1544     }
   1545     guard let runID = runIDValue,
   1546       runID.range(of: "^[a-z0-9][a-z0-9-]{6,62}[a-z0-9]$", options: .regularExpression) != nil,
   1547       let blossom = blossomValue,
   1548       !blossom.isEmpty,
   1549       let networkProfile,
   1550       networkProfile == "public" || networkProfile == "simulator"
   1551     else {
   1552       throw QualificationError.missingEnvironment
   1553     }
   1554     _ = try QualificationEndpointPolicy(
   1555       relayURLs: separated(relayValue),
   1556       blossomOrigins: separated(blossom),
   1557       profile: networkProfile
   1558     )
   1559     if networkProfile == "simulator" {
   1560       guard
   1561         let fixtureControl,
   1562         let fixtureEvidence,
   1563         fixtureControl.utf8.count <= 1024,
   1564         fixtureEvidence.utf8.count <= 1024,
   1565         fixtureControl.hasPrefix("/"),
   1566         fixtureEvidence.hasPrefix("/")
   1567       else {
   1568         throw QualificationError.missingEnvironment
   1569       }
   1570     }
   1571     let evidenceBinding = PersonaEvidenceBinding(
   1572       sourceCommit: sourceCommit,
   1573       sourceTree: sourceTree,
   1574       appBuildSHA256: appBuildSHA256,
   1575       simulatorID: simulatorID
   1576     )
   1577     return Self(
   1578       runID: runID,
   1579       relayURLs: separated(relayValue),
   1580       blossomOrigins: separated(blossom),
   1581       fixtureControl: fixtureControl.flatMap { $0.isEmpty ? nil : $0 },
   1582       fixtureEvidence: fixtureEvidence.flatMap { $0.isEmpty ? nil : $0 },
   1583       networkProfile: networkProfile,
   1584       evidenceBinding: evidenceBinding
   1585     )
   1586   }
   1587 
   1588   private static func value(
   1589     _ key: String,
   1590     values: [String: String],
   1591     bundle: Bundle
   1592   ) -> String? {
   1593     (values[key] ?? bundle.object(forInfoDictionaryKey: key) as? String)
   1594       .flatMap { $0.isEmpty ? nil : $0 }
   1595   }
   1596 
   1597   private static func separated(_ value: String?) -> [String] {
   1598     (value ?? "").split(separator: ",").map(String.init).filter { !$0.isEmpty }
   1599   }
   1600 }
   1601 
   1602 private struct PersonaEvidenceBinding {
   1603   let sourceCommit: String
   1604   let sourceTree: String
   1605   let appBuildSHA256: String
   1606   let simulatorID: String
   1607 
   1608   init?(
   1609     sourceCommit: String?,
   1610     sourceTree: String?,
   1611     appBuildSHA256: String?,
   1612     simulatorID: String?
   1613   ) {
   1614     guard let sourceCommit,
   1615       sourceCommit.range(of: "^[0-9a-f]{40}$", options: .regularExpression) != nil,
   1616       let sourceTree,
   1617       sourceTree.range(of: "^[0-9a-f]{40}$", options: .regularExpression) != nil,
   1618       let appBuildSHA256,
   1619       appBuildSHA256.range(of: "^[0-9a-f]{64}$", options: .regularExpression) != nil,
   1620       let simulatorID,
   1621       simulatorID.uppercased().range(
   1622         of: "^[A-F0-9-]{36}$",
   1623         options: .regularExpression
   1624       ) != nil
   1625     else { return nil }
   1626     self.sourceCommit = sourceCommit
   1627     self.sourceTree = sourceTree
   1628     self.appBuildSHA256 = appBuildSHA256
   1629     self.simulatorID = simulatorID.uppercased()
   1630   }
   1631 }
   1632 
   1633 private struct PersonaAttemptTestInvocation: Encodable {
   1634   let target: String
   1635   let identifier: String
   1636   let action: String
   1637   let configuration: String
   1638 }
   1639 
   1640 private struct PersonaAttemptSource: Encodable {
   1641   let commit: String
   1642   let tree: String
   1643 }
   1644 
   1645 private struct PersonaAttemptSimulator: Encodable {
   1646   let udid: String
   1647   let os: String
   1648   let architecture: String
   1649 }
   1650 
   1651 private struct PersonaAttemptUIObservation: Encodable {
   1652   let validationAttempted: Bool
   1653   let validationRejected: Bool
   1654   let retryAttempts: Int
   1655   let relaunches: Int
   1656   let retentionVerified: Bool
   1657   let todayProjectionVerified: Bool
   1658 
   1659   enum CodingKeys: String, CodingKey {
   1660     case validationAttempted = "validation_attempted"
   1661     case validationRejected = "validation_rejected"
   1662     case retryAttempts = "retry_attempts"
   1663     case relaunches
   1664     case retentionVerified = "retention_verified"
   1665     case todayProjectionVerified = "today_projection_verified"
   1666   }
   1667 }
   1668 
   1669 private struct PersonaInteractionObservation {
   1670   let validationAttempted: Bool
   1671   let validationRejected: Bool
   1672   let retryAttempts: Int
   1673   let relaunches: Int
   1674   let progressiveDisclosure: Bool
   1675   let accessibilitySemantics: Bool
   1676   let keyboardFocus: Bool
   1677   let visibleActions: Bool
   1678 }
   1679 
   1680 private struct PersonaAttemptObservation {
   1681   let interaction: PersonaInteractionObservation
   1682   let network: PersonaAttemptNetworkObservation
   1683   let todayProjectionVerified: Bool
   1684   let retentionVerified: Bool
   1685 
   1686   var ui: PersonaAttemptUIObservation {
   1687     PersonaAttemptUIObservation(
   1688       validationAttempted: interaction.validationAttempted,
   1689       validationRejected: interaction.validationRejected,
   1690       retryAttempts: interaction.retryAttempts,
   1691       relaunches: interaction.relaunches,
   1692       retentionVerified: retentionVerified,
   1693       todayProjectionVerified: todayProjectionVerified
   1694     )
   1695   }
   1696 
   1697   func confirmingRetention() -> Self {
   1698     Self(
   1699       interaction: interaction,
   1700       network: network,
   1701       todayProjectionVerified: todayProjectionVerified,
   1702       retentionVerified: true
   1703     )
   1704   }
   1705 }
   1706 
   1707 private struct PersonaAttemptNetworkObservation: Encodable {
   1708   let state: String
   1709   let acceptedConnections: Int
   1710   let rejectedConnections: Int
   1711   let nonLoopbackAttempts: Int
   1712   let subscriptions: Int
   1713   let acceptedEvents: Int
   1714   let acceptedUploads: Int
   1715   let retrievals: Int
   1716   let unintendedPublications: Int
   1717   let eventsAcceptedDuringExpectedFailure: Int
   1718   let finalCandidateDataLoss: Int
   1719 
   1720   enum CodingKeys: String, CodingKey {
   1721     case state
   1722     case acceptedConnections = "accepted_connections"
   1723     case rejectedConnections = "rejected_connections"
   1724     case nonLoopbackAttempts = "non_loopback_attempts"
   1725     case subscriptions
   1726     case acceptedEvents = "accepted_events"
   1727     case acceptedUploads = "accepted_uploads"
   1728     case retrievals
   1729     case unintendedPublications = "unintended_publications"
   1730     case eventsAcceptedDuringExpectedFailure = "events_accepted_during_expected_failure"
   1731     case finalCandidateDataLoss = "final_candidate_data_loss"
   1732   }
   1733 }
   1734 
   1735 private struct PersonaFixtureNetworkSnapshot {
   1736   private static let keys: Set<String> = [
   1737     "schema", "schema_version", "personas", "flow_counts", "accepted_events",
   1738     "event_kind_counts", "upload_attempts", "accepted_uploads", "retrievals",
   1739     "distinct_identities", "unknown_attempts", "duplicate_attempts",
   1740     "expected_failure_rejections", "events_accepted_during_expected_failures",
   1741     "accepted_connections", "rejected_connections", "non_loopback_attempts",
   1742     "production_network_contacts", "unintended_publications", "final_candidate_data_loss",
   1743   ]
   1744 
   1745   let acceptedConnections: Int
   1746   let rejectedConnections: Int
   1747   let nonLoopbackAttempts: Int
   1748   let productionNetworkContacts: Int
   1749   let subscriptions: Int
   1750   let acceptedEvents: Int
   1751   let acceptedUploads: Int
   1752   let retrievals: Int
   1753   let unintendedPublications: Int
   1754   let eventsAcceptedDuringExpectedFailures: Int
   1755   let finalCandidateDataLoss: Int
   1756 
   1757   init(_ value: [String: Any]) throws {
   1758     guard Set(value.keys) == Self.keys,
   1759       value["schema"] as? String == "tera.ios.local-social.persona-evidence.v1",
   1760       value["schema_version"] as? Int == 1,
   1761       let acceptedConnections = value["accepted_connections"] as? Int,
   1762       let rejectedConnections = value["rejected_connections"] as? Int,
   1763       let nonLoopbackAttempts = value["non_loopback_attempts"] as? Int,
   1764       let productionNetworkContacts = value["production_network_contacts"] as? Int,
   1765       let acceptedEvents = value["accepted_events"] as? Int,
   1766       let acceptedUploads = value["accepted_uploads"] as? Int,
   1767       let retrievals = value["retrievals"] as? Int,
   1768       let unintendedPublications = value["unintended_publications"] as? Int,
   1769       let eventsAccepted = value["events_accepted_during_expected_failures"] as? Int,
   1770       let finalCandidateDataLoss = value["final_candidate_data_loss"] as? Int,
   1771       let personas = value["personas"] as? [[String: Any]],
   1772       personas.count == 5,
   1773       [
   1774         acceptedConnections, rejectedConnections, nonLoopbackAttempts,
   1775         productionNetworkContacts,
   1776         acceptedEvents, acceptedUploads, retrievals, unintendedPublications,
   1777         eventsAccepted, finalCandidateDataLoss,
   1778       ].allSatisfy({ 0 ... 61440 ~= $0 })
   1779     else {
   1780       throw QualificationError.invalidFixtureEvidence
   1781     }
   1782     let subscriptions = try personas.reduce(into: 0) { total, persona in
   1783       guard let value = persona["subscriptions"] as? Int, 0 ... 4096 ~= value else {
   1784         throw QualificationError.invalidFixtureEvidence
   1785       }
   1786       total += value
   1787     }
   1788     self.acceptedConnections = acceptedConnections
   1789     self.rejectedConnections = rejectedConnections
   1790     self.nonLoopbackAttempts = nonLoopbackAttempts
   1791     self.productionNetworkContacts = productionNetworkContacts
   1792     self.subscriptions = subscriptions
   1793     self.acceptedEvents = acceptedEvents
   1794     self.acceptedUploads = acceptedUploads
   1795     self.retrievals = retrievals
   1796     self.unintendedPublications = unintendedPublications
   1797     eventsAcceptedDuringExpectedFailures = eventsAccepted
   1798     self.finalCandidateDataLoss = finalCandidateDataLoss
   1799   }
   1800 
   1801   func delta(from prior: Self) throws -> PersonaAttemptNetworkObservation {
   1802     let current = [
   1803       acceptedConnections, rejectedConnections, nonLoopbackAttempts, subscriptions,
   1804       productionNetworkContacts,
   1805       acceptedEvents, acceptedUploads, retrievals, unintendedPublications,
   1806       eventsAcceptedDuringExpectedFailures, finalCandidateDataLoss,
   1807     ]
   1808     let previous = [
   1809       prior.acceptedConnections, prior.rejectedConnections, prior.nonLoopbackAttempts,
   1810       prior.subscriptions, prior.productionNetworkContacts, prior.acceptedEvents,
   1811       prior.acceptedUploads, prior.retrievals,
   1812       prior.unintendedPublications, prior.eventsAcceptedDuringExpectedFailures,
   1813       prior.finalCandidateDataLoss,
   1814     ]
   1815     guard zip(current, previous).allSatisfy({ pair in pair.0 >= pair.1 }) else {
   1816       throw QualificationError.invalidFixtureEvidence
   1817     }
   1818     let values = zip(current, previous).map { pair in pair.0 - pair.1 }
   1819     guard values[2] == 0, values[4] == 0 else {
   1820       throw QualificationError.invalidEndpointPolicy
   1821     }
   1822     return PersonaAttemptNetworkObservation(
   1823       state: "measured",
   1824       acceptedConnections: values[0],
   1825       rejectedConnections: values[1],
   1826       nonLoopbackAttempts: values[2],
   1827       subscriptions: values[3],
   1828       acceptedEvents: values[5],
   1829       acceptedUploads: values[6],
   1830       retrievals: values[7],
   1831       unintendedPublications: values[8],
   1832       eventsAcceptedDuringExpectedFailure: values[9],
   1833       finalCandidateDataLoss: values[10]
   1834     )
   1835   }
   1836 }
   1837 
   1838 private struct PersonaAttemptAccessibility: Encodable {
   1839   let locale: String
   1840   let contentSize: String
   1841   let reduceMotion: Bool
   1842   let progressiveDisclosure: Bool
   1843   let labelsValuesTraits: Bool
   1844   let keyboardFocus: Bool
   1845   let visibleActions: Bool
   1846   let voiceoverUserObserved: Bool
   1847 
   1848   enum CodingKeys: String, CodingKey {
   1849     case locale
   1850     case contentSize = "content_size"
   1851     case reduceMotion = "reduce_motion"
   1852     case progressiveDisclosure = "progressive_disclosure"
   1853     case labelsValuesTraits = "labels_values_traits"
   1854     case keyboardFocus = "keyboard_focus"
   1855     case visibleActions = "visible_actions"
   1856     case voiceoverUserObserved = "voiceover_user_observed"
   1857   }
   1858 }
   1859 
   1860 private struct PersonaAttemptArtifactDigest: Encodable {
   1861   let role: String
   1862   let sha256: String
   1863 }
   1864 
   1865 private struct PersonaAttemptEvidence: Encodable {
   1866   let schema: String
   1867   let schemaVersion: UInt16
   1868   let testInvocation: PersonaAttemptTestInvocation
   1869   let source: PersonaAttemptSource
   1870   let appBuildSHA256: String
   1871   let simulator: PersonaAttemptSimulator
   1872   let runID: String
   1873   let personaRunID: String
   1874   let personaAlias: String
   1875   let attemptID: String
   1876   let attemptOrder: Int
   1877   let flow: String
   1878   let expectedFailure: String
   1879   let publicIdentitySHA256: String
   1880   let endpointPolicySHA256: String
   1881   let uiObservation: PersonaAttemptUIObservation
   1882   let networkObservation: PersonaAttemptNetworkObservation
   1883   let accessibility: PersonaAttemptAccessibility
   1884   let artifactDigests: [PersonaAttemptArtifactDigest]
   1885 
   1886   enum CodingKeys: String, CodingKey {
   1887     case schema
   1888     case schemaVersion = "schema_version"
   1889     case testInvocation = "test_invocation"
   1890     case source
   1891     case appBuildSHA256 = "app_build_sha256"
   1892     case simulator
   1893     case runID = "run_id"
   1894     case personaRunID = "persona_run_id"
   1895     case personaAlias = "persona_alias"
   1896     case attemptID = "attempt_id"
   1897     case attemptOrder = "attempt_order"
   1898     case flow
   1899     case expectedFailure = "expected_failure"
   1900     case publicIdentitySHA256 = "public_identity_sha256"
   1901     case endpointPolicySHA256 = "endpoint_policy_sha256"
   1902     case uiObservation = "ui_observation"
   1903     case networkObservation = "network_observation"
   1904     case accessibility
   1905     case artifactDigests = "artifact_digests"
   1906   }
   1907 }
   1908 
   1909 private struct BootstrapReceipt: Codable {
   1910   let schema: String
   1911   let schemaVersion: UInt16
   1912   let runID: String
   1913   let publicKey: String
   1914   let relayURLs: [String]
   1915   let blossomOrigins: [String]
   1916   let privateKeyPresent: Bool
   1917   let interactiveAuthenticationRequired: Bool
   1918 }
   1919 
   1920 private struct PersonaSuite: Decodable {
   1921   let schema: String
   1922   let schemaVersion: UInt16
   1923   let locale: String
   1924   let mediaFixtureSHA256: String
   1925   let personas: [Persona]
   1926 
   1927   enum CodingKeys: String, CodingKey {
   1928     case schema
   1929     case schemaVersion = "schema_version"
   1930     case locale
   1931     case mediaFixtureSHA256 = "media_fixture_sha256"
   1932     case personas
   1933   }
   1934 }
   1935 
   1936 private struct Persona: Decodable {
   1937   let alias: String
   1938   let syntheticAgeBand: String
   1939   let interactionProfile: String
   1940   let attempts: [PersonaAttempt]
   1941 
   1942   enum CodingKeys: String, CodingKey {
   1943     case alias
   1944     case syntheticAgeBand = "synthetic_age_band"
   1945     case interactionProfile = "interaction_profile"
   1946     case attempts
   1947   }
   1948 }
   1949 
   1950 private struct PersonaAttempt: Decodable {
   1951   let id: String
   1952   let order: Int
   1953   let flow: PersonaFlow
   1954   let marker: String
   1955   let expectedFailure: String
   1956 
   1957   enum CodingKeys: String, CodingKey {
   1958     case id
   1959     case order
   1960     case flow
   1961     case marker
   1962     case expectedFailure = "expected_failure"
   1963   }
   1964 }
   1965 
   1966 private enum PersonaFlow: String, Decodable {
   1967   case update = "Update"
   1968   case photoUpdate = "PhotoUpdate"
   1969   case ask = "Ask"
   1970   case event = "Event"
   1971   case foodAvailability = "FoodAvailability"
   1972 
   1973   var uiLabel: String {
   1974     switch self {
   1975     case .update: "Update"
   1976     case .photoUpdate: "Photo update"
   1977     case .ask: "Ask"
   1978     case .event: "Event"
   1979     case .foodAvailability: "Food availability"
   1980     }
   1981   }
   1982 }
   1983 
   1984 private enum QualificationError: Error {
   1985   case missingEnvironment
   1986   case invalidEndpointPolicy
   1987   case invalidFixtureEvidence
   1988   case invalidPublicKey
   1989   case missingProductSurface
   1990   case productSubmissionFailed
   1991   case invalidPersonaFixture
   1992 }