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 }