TeraNativeRepairTests.swift (6603B)
1 import Foundation 2 import RadrootsKit 3 @testable import TeraApp 4 import TeraKitBindings 5 import XCTest 6 7 final class TeraNativeRepairTests: XCTestCase { 8 func testMissingAndMismatchedTransfersPersistIndependentlyOfValidSettlement() async throws { 9 let fixture = try BackgroundUploadFixture() 10 let storage = try MediaOwnershipFixture() 11 defer { fixture.remove(); storage.remove() } 12 let runtime = try await storage.runtime() 13 let backend = TeraGeneratedRuntimeBackend(runtime: runtime) 14 let transfer = BackgroundTransferHarness() 15 let request = try fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32))) 16 let missing = try RadrootsBackgroundTransferIdentifier("radroots.add.\(String(repeating: "0", count: 31))1.2.\(String(repeating: "a", count: 32))") 17 try await transfer.seed(request: TeraBackgroundUploadRequest.replacingIdentifier(in: request, with: missing), state: .awaitingVerification) 18 try await transfer.seed(request: request, state: .awaitingVerification) 19 let mismatch = try fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "b", count: 32)), remoteURL: "http://127.0.0.1:3000/changed") 20 try await transfer.seed(request: mismatch, state: .awaitingVerification) 21 let owner = TeraNativeUploadRecoveryOwner(draft: fixture.draft(revision: 3, stage: .verified)) 22 let run = { @Sendable in 23 try await TeraNativeRecoveryInventory.run(transfer: transfer, cursor: nil, complete: { snapshot, owner in 24 let input = try TeraRecoveryUploadReceipt(snapshot: snapshot, owner: owner) 25 try await TeraNativeUploadReconciliation.settle(snapshot, input: input, receipt: RecoverySettlementFixture.receipt(input), transfer: transfer) 26 }, report: { snapshot, reason in 27 let key = TeraNativeRecoveryIssue.key(snapshot.identifier.rawValue) 28 let status = try? await backend.reportNativeRecoveryStatus(key: key, reason: reason) 29 return reason == .resolved ? nil : .init(key: key, reason: reason, status: status) 30 }, lookup: { key in key == fixture.draftID ? owner : nil }) 31 } 32 let first = try await run() 33 XCTAssertEqual(first.progress.visited, 3) 34 XCTAssertEqual(first.progress.issues.count, 2) 35 XCTAssertTrue(first.progress.issues.contains { $0.reason == .missingParent }) 36 XCTAssertTrue(first.progress.issues.contains { $0.reason == .associationMismatch }) 37 XCTAssertTrue(first.progress.issues.allSatisfy { $0.status?.revision == 1 }) 38 let replay = try await run() 39 XCTAssertEqual(replay.progress.visited, 3, "Completed positions are visited without repeating settlement") 40 XCTAssertEqual(replay.progress.issues, first.progress.issues) 41 let counts = await transfer.counts 42 XCTAssertEqual(counts.acceptedSettlement, 1) 43 XCTAssertEqual(counts.cancel, 0) 44 XCTAssertEqual(counts.enqueue, 0) 45 for issue in first.progress.issues { 46 let saved = try await backend.nativeRecoveryStatus(key: issue.key) 47 XCTAssertEqual(saved, issue.status) 48 } 49 _ = try await runtime.shutdown() 50 } 51 52 func testProtectedDataPausesAtSameCursorWithoutCorruptionOrSettlement() async throws { 53 let fixture = try BackgroundUploadFixture() 54 defer { fixture.remove() } 55 let transfer = BackgroundTransferHarness() 56 try await transfer.seed(request: fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32))), state: .awaitingVerification) 57 let failure = TeraRuntimeFailure.local(operation: "test.recovery", code: "protected_data_unavailable", safeMessage: "Locked.") 58 let result = try await TeraNativeRecoveryInventory.run(transfer: transfer, cursor: nil) { _ in throw failure } 59 XCTAssertEqual(result.progress.pause, .protectedData) 60 XCTAssertEqual(result.progress.visited, 0) 61 XCTAssertEqual(result.progress.remaining, 1) 62 XCTAssertFalse(result.progress.needsAttention) 63 XCTAssertTrue(result.progress.issues.isEmpty) 64 XCTAssertNil(result.cursor) 65 let state = await transfer.state 66 XCTAssertEqual(state, .awaitingVerification) 67 } 68 69 @MainActor 70 func testRunningNativeRecoveryCannotBlockUnrelatedSaveAndSubmit() async throws { 71 let backend = AddBackend(advanceOffline: true) 72 let client = try await TeraAddStoreTests.startedClient(backend) 73 let fixture = try BackgroundUploadFixture() 74 defer { fixture.remove() } 75 let transfer = BackgroundTransferHarness() 76 let pause = ResourceTestPause() 77 await transfer.pauseDiscovery(pause) 78 let store = TeraAddStore(runtimeClient: client, media: fixture.coordinator(transfer: transfer)) 79 await store.configure(snapshot: backend.snapshot()) 80 let startup = Task { await store.start() } 81 await pause.entered.wait() 82 XCTAssertEqual(store.state, .ready) 83 store.updateForm(\.content, "An unrelated saved update") 84 await store.save() 85 XCTAssertEqual(store.composerState, .saved) 86 await store.submit() 87 XCTAssertEqual(store.submissions.status?.state, .queued) 88 XCTAssertTrue(store.recovery.transfers.isRunning) 89 await pause.resume.open() 90 await startup.value 91 XCTAssertEqual(store.state, .ready) 92 store.stop() 93 _ = try await client.stop() 94 } 95 96 @MainActor 97 func testFailedStatusPersistenceIsNotReportedAsDurable() async throws { 98 let backend = try TeraScopeBackend() 99 let client = try await TeraScopeFixtures.client(backend) 100 let issue = await TeraNativeRecoveryClassification.report("unknown-native-record", reason: .missingParent, client: client) 101 XCTAssertEqual(issue?.reason, .missingParent) 102 XCTAssertNil(issue?.status) 103 XCTAssertEqual(TeraNativeRecoveryClassification.pause(RadrootsBackgroundTransferError.persistenceFailure), .storageUnavailable) 104 _ = try await client.stop() 105 } 106 107 func testGeneratedStatusRejectsUnknownVersionAndNoncanonicalIdentity() async throws { 108 let fixture = try MediaOwnershipFixture() 109 defer { fixture.remove() } 110 let runtime = try await fixture.runtime() 111 for (version, key) in [(UInt16(2), String(repeating: "a", count: 64)), (1, String(repeating: "A", count: 64))] { 112 do { 113 _ = try await runtime.reportNativeRecoveryStatus(schemaVersion: version, transferKey: key, reason: .missingParent) 114 XCTFail("Invalid status input must fail before persistence") 115 } catch let TeraAppError.Failure(report) { 116 XCTAssertEqual(report.code, "invalid_native_recovery_status") 117 } 118 } 119 let absent = try await runtime.nativeRecoveryStatus(schemaVersion: 1, transferKey: String(repeating: "a", count: 64)) 120 XCTAssertNil(absent) 121 _ = try await runtime.shutdown() 122 } 123 }