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 }