field_ios

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

TeraRecoveryContinuationTests.swift (4718B)


      1 import Foundation
      2 import RadrootsKit
      3 @testable import TeraApp
      4 import TeraKitBindings
      5 import XCTest
      6 
      7 final class TeraRecoveryContinuationTests: XCTestCase {
      8   func testGeneratedCursorSurvivesRuntimeReopenAndRejectsStaleOrMalformedInput() async throws {
      9     let fixture = try MediaOwnershipFixture()
     10     defer { fixture.remove() }
     11     let runtime = try await fixture.runtime()
     12     let backend = TeraGeneratedRuntimeBackend(runtime: runtime)
     13     let first = try await backend.nativeRecoverySchedule()
     14     let saved = try await backend.advanceNativeRecoverySchedule(expected: first, after: String(repeating: "b", count: 64))
     15     XCTAssertEqual(saved.revision, 1)
     16     _ = try await runtime.shutdown()
     17     let reopened = try await fixture.runtime()
     18     let next = TeraGeneratedRuntimeBackend(runtime: reopened)
     19     let retained = try await next.nativeRecoverySchedule()
     20     XCTAssertEqual(retained, saved)
     21     do {
     22       _ = try await next.advanceNativeRecoverySchedule(expected: first, after: nil)
     23       XCTFail("Stale observation cannot rewind traversal")
     24     } catch {}
     25     for key in ["", String(repeating: "B", count: 64), String(repeating: "b", count: 65)] {
     26       do {
     27         _ = try await next.advanceNativeRecoverySchedule(expected: saved, after: key)
     28         XCTFail("Malformed cursor must fail")
     29       } catch {}
     30     }
     31     let reset = try await next.advanceNativeRecoverySchedule(expected: saved, after: nil)
     32     XCTAssertEqual(reset.revision, 2)
     33     XCTAssertNil(reset.after)
     34     _ = try await reopened.shutdown()
     35   }
     36 
     37   func testInterruptedBatchPreservesVisitedPositionAndRetainsUnconfirmedReceipts() async throws {
     38     let fixture = try BackgroundUploadFixture()
     39     defer { fixture.remove() }
     40     let transfer = try await Self.inventory(fixture, count: 130)
     41     let cursor = NativeRecoveryScheduleTestStorage()
     42     let lookups = RecoveryContinuationLookups()
     43     let pause = ResourceTestPause()
     44     let worker = Task {
     45       try await TeraNativeRecoveryInventory.run(transfer: transfer, cursor: nil, checkpoint: { key in
     46         let prior = await cursor.load()
     47         _ = try await cursor.advance(expected: prior, after: key)
     48       }) { key in
     49         if await lookups.record(key) == 2 {
     50           await pause.wait()
     51         }
     52         return nil
     53       }
     54     }
     55     await pause.entered.wait()
     56     worker.cancel()
     57     await pause.resume.open()
     58     do { _ = try await worker.value; XCTFail("Cancelled batch must stop") } catch is CancellationError {}
     59     let saved = await cursor.load()
     60     XCTAssertNotNil(saved.after)
     61     let resumed = try await TeraNativeRecoveryInventory.run(transfer: transfer, cursor: saved.after) { _ in nil }
     62     XCTAssertEqual(resumed.progress.visited, 64)
     63     XCTAssertEqual(resumed.progress.remaining, 65)
     64     let counts = await transfer.counts
     65     XCTAssertEqual(counts.acceptedSettlement, 0)
     66     XCTAssertEqual(counts.enqueue, 0)
     67     let snapshots = try await transfer.snapshots()
     68     XCTAssertEqual(snapshots.count, 130)
     69   }
     70 
     71   @MainActor
     72   func testFreshCoordinatorContinuesPastQuarantinedPrefix() async throws {
     73     let fixture = try BackgroundUploadFixture()
     74     defer { fixture.remove() }
     75     let transfer = try await Self.inventory(fixture, count: 130)
     76     let backend = try TeraScopeBackend()
     77     await backend.setDrafts([])
     78     let client = try await TeraScopeFixtures.client(backend)
     79     for remaining in [66, 2, 0] {
     80       let freshOwner = fixture.coordinator(transfer: transfer)
     81       let progress = try await freshOwner.recoverNativeUploads(client: client)
     82       XCTAssertEqual(progress.remaining, remaining)
     83       XCTAssertTrue(progress.needsAttention)
     84       XCTAssertLessThanOrEqual(progress.visited, 64)
     85     }
     86     let cursor = try await client.nativeRecoverySchedule()
     87     XCTAssertNil(cursor.after)
     88     let snapshots = try await transfer.snapshots()
     89     XCTAssertEqual(snapshots.count, 130, "Traversal never deletes quarantined evidence")
     90     _ = try await client.stop()
     91   }
     92 
     93   private static func inventory(_ fixture: BackgroundUploadFixture, count: Int) async throws -> BackgroundTransferHarness {
     94     let transfer = BackgroundTransferHarness()
     95     let request = try fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32)))
     96     for index in 1 ... count {
     97       let identifier = try RadrootsBackgroundTransferIdentifier("radroots.add.\(String(format: "%032x", index)).2.\(String(repeating: "a", count: 32))")
     98       try await transfer.seed(request: TeraBackgroundUploadRequest.replacingIdentifier(in: request, with: identifier), state: .awaitingVerification)
     99     }
    100     return transfer
    101   }
    102 }
    103 
    104 private actor RecoveryContinuationLookups {
    105   private var count = 0
    106   func record(_: String) -> Int {
    107     count += 1; return count
    108   }
    109 }