field_ios

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

TeraOperationRecoveryTests.swift (7202B)


      1 import Foundation
      2 import RadrootsKit
      3 @testable import TeraApp
      4 import XCTest
      5 
      6 final class TeraOperationRecoveryTests: XCTestCase {
      7   @MainActor
      8   func testSelectedTransferUsesExactKeyWithoutMovingSweepOrAdmittingSecondWorker() async throws {
      9     let fixture = try BackgroundUploadFixture()
     10     defer { fixture.remove() }
     11     let transfer = BackgroundTransferHarness()
     12     let selected = try fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32)))
     13     let other = try fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "b", count: 32)))
     14     try await transfer.seed(request: selected, state: .awaitingVerification)
     15     try await transfer.seed(request: other, state: .awaitingVerification)
     16     let backend = try TeraScopeBackend()
     17     let client = try await TeraScopeFixtures.client(backend)
     18     let coordinator = fixture.coordinator(transfer: transfer)
     19     let prior = try await client.nativeRecoverySchedule()
     20     let key = TeraNativeRecoveryIssue.key(selected.identifier.rawValue)
     21     let pause = ResourceTestPause()
     22     await transfer.pauseDiscovery(pause)
     23     let task = Task { try await coordinator.recoverNativeUpload(key: key, client: client) }
     24     await pause.entered.wait()
     25     do {
     26       _ = try await coordinator.recoverNativeUploads(client: client)
     27       XCTFail("Selected check must retain the same recovery admission")
     28     } catch {}
     29     await pause.resume.open()
     30     let result = try await task.value
     31     XCTAssertEqual(result.visited, 1)
     32     XCTAssertEqual(result.remaining, 0)
     33     XCTAssertEqual(result.issues.map(\.key), [key])
     34     let after = try await client.nativeRecoverySchedule()
     35     XCTAssertEqual(after, prior)
     36     for absent in [String(repeating: "0", count: 64), "invalid"] {
     37       do {
     38         _ = try await coordinator.recoverNativeUpload(key: absent, client: client)
     39         XCTFail("An absent or invalid selected record is not success")
     40       } catch {}
     41     }
     42     let counts = await transfer.counts
     43     XCTAssertEqual(counts.enqueue, 0)
     44     XCTAssertEqual(counts.acceptedSettlement, 0)
     45     _ = try await client.stop()
     46   }
     47 
     48   func testCompletedTransferRepairsLostAdvisoryWriteAfterRuntimeReopen() async throws {
     49     let fixture = try BackgroundUploadFixture()
     50     let storage = try MediaOwnershipFixture()
     51     defer { fixture.remove(); storage.remove() }
     52     let transfer = BackgroundTransferHarness()
     53     let request = try fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32)))
     54     try await transfer.seed(request: request, state: .awaitingVerification)
     55     let key = TeraNativeRecoveryIssue.key(request.identifier.rawValue)
     56     let first = try await storage.runtime()
     57     let backend = TeraGeneratedRuntimeBackend(runtime: first)
     58     _ = try await backend.reportNativeRecoveryStatus(key: key, reason: .outcomeUnconfirmed)
     59     let owner = TeraNativeUploadRecoveryOwner(draft: fixture.draft(revision: 3, stage: .verified))
     60     // Settle native evidence, then model the lost advisory write by retaining it.
     61     _ = try await RecoverySettlementFixture.run(transfer: transfer, cursor: nil) { _ in owner }
     62     _ = try await first.shutdown()
     63     let reopened = try await storage.runtime()
     64     let next = TeraGeneratedRuntimeBackend(runtime: reopened)
     65     let inspection = TeraNativeRecoveryInspection(completedNeedsRepair: { key in
     66       let status = try await next.nativeRecoveryStatus(key: key)
     67       return status != nil && status?.reason != .resolved
     68     })
     69     let result = try await TeraNativeRecoveryInventory.run(transfer: transfer, cursor: nil, inspection: inspection, complete: { snapshot, exactOwner in
     70       let input = try TeraRecoveryUploadReceipt(snapshot: snapshot, owner: exactOwner)
     71       try await TeraNativeUploadReconciliation.settle(snapshot, input: input, receipt: RecoverySettlementFixture.receipt(input), transfer: transfer)
     72     }, report: { snapshot, reason in
     73       let key = TeraNativeRecoveryIssue.key(snapshot.identifier.rawValue)
     74       let status = try? await next.reportNativeRecoveryStatus(key: key, reason: reason)
     75       return .init(key: key, reason: reason, status: status)
     76     }, lookup: { _ in owner })
     77     XCTAssertEqual(result.progress.issues.map(\.reason), [.resolved])
     78     let saved = try await next.nativeRecoveryStatus(key: key)
     79     XCTAssertEqual(saved?.reason, .resolved)
     80     XCTAssertEqual(saved?.revision, 2)
     81     let counts = await transfer.counts
     82     XCTAssertEqual(counts.acceptedSettlement, 1)
     83     XCTAssertEqual(counts.enqueue, 0)
     84     XCTAssertEqual(counts.retry, 0)
     85     _ = try await reopened.shutdown()
     86   }
     87 
     88   func testCompletedEvidenceStillRequiresExactParentAndAssociation() async throws {
     89     let fixture = try BackgroundUploadFixture()
     90     defer { fixture.remove() }
     91     let transfer = BackgroundTransferHarness()
     92     let request = try fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32)))
     93     try await transfer.seed(request: request, state: .completed)
     94     let inspection = TeraNativeRecoveryInspection(completedNeedsRepair: { _ in true })
     95     let missing = try await TeraNativeRecoveryInventory.run(transfer: transfer, cursor: nil, inspection: inspection) { _ in nil }
     96     XCTAssertEqual(missing.progress.issues.map(\.reason), [.missingParent])
     97     let mismatch = try await TeraNativeRecoveryInventory.run(transfer: transfer, cursor: nil, inspection: inspection, complete: { _, _ in
     98       throw TeraNativeRecoveryFault.associationMismatch
     99     }) { _ in .init(draft: fixture.draft(revision: 3, stage: .verified)) }
    100     XCTAssertEqual(mismatch.progress.issues.map(\.reason), [.associationMismatch])
    101     let counts = await transfer.counts
    102     XCTAssertEqual(counts.acceptedSettlement, 0)
    103     XCTAssertEqual(counts.cancel, 0)
    104   }
    105 
    106   @MainActor
    107   func testFailedResolvedReportCannotClearNoticeAndPauseReasonsRemainDistinct() async throws {
    108     let backend = try TeraScopeBackend()
    109     let client = try await TeraScopeFixtures.client(backend)
    110     let result = await TeraNativeRecoveryClassification.report("retained", reason: .resolved, client: client)
    111     XCTAssertNil(result)
    112     let saved = TeraNativeRecoveryStatus(key: TeraNativeRecoveryIssue.key("retained"), reason: .outcomeUnconfirmed,
    113                                          revision: 1, firstObservedUnixMS: 1, updatedAtUnixMS: 1)
    114     await backend.setNativeRepairValue(saved)
    115     let unresolved = await TeraNativeRecoveryClassification.report("retained", reason: .resolved, client: client)
    116     XCTAssertEqual(unresolved, .init(key: saved.key, reason: saved.reason, status: saved), "A fresh store must see a retained advisory when resolution fails")
    117     let reasons: [(String, TeraNativeRecoveryPause)] = [
    118       ("protected_data_unavailable", .protectedData), ("today_media_quota_exceeded", .quota),
    119       ("identity_unavailable", .credentials), ("today_runtime_unavailable", .runtimeUnavailable),
    120       ("store_path_unavailable", .storageUnavailable),
    121     ]
    122     for (code, pause) in reasons {
    123       let failure = TeraRuntimeFailure.local(operation: "test.recovery", code: code, safeMessage: "Paused.")
    124       XCTAssertEqual(TeraNativeRecoveryClassification.pause(failure), pause)
    125     }
    126     _ = try await client.stop()
    127   }
    128 }