TeraPublicationRetryTests.swift (3837B)
1 import Foundation 2 @testable import TeraApp 3 import TeraKitBindings 4 import XCTest 5 6 @MainActor 7 final class TeraPublicationRetryTests: XCTestCase { 8 func testTypedDecisionsRetainSpecificActionAndWaitingReasons() throws { 9 let cases: [(FfiPublicationActionReason, TeraPublicationActionReason)] = [ 10 (.deadlineExceeded, .deadlineExceeded), (.attemptLimit, .attemptLimit), 11 (.authenticationRequired, .authenticationRequired), (.quotaExceeded, .quotaExceeded), 12 (.invalidPayload, .invalidPayload), (.deliveryRefused, .deliveryRefused), 13 (.coordinateChanged, .coordinateChanged), 14 ] 15 for (wire, expected) in cases { 16 let decoded = try TeraPublicationRetry.decode(.needsAction(reason: wire)) 17 XCTAssertEqual(decoded, .needsAction(expected)) 18 XCTAssertFalse(decoded.mayStart) 19 XCTAssertTrue(decoded.explanation?.contains("retain") == true) 20 } 21 XCTAssertTrue(try TeraPublicationRetry.decode(.ready).mayStart) 22 XCTAssertFalse(try TeraPublicationRetry.decode(.complete).mayStart) 23 XCTAssertFalse(try TeraPublicationRetry.decode(.stopped).mayStart) 24 XCTAssertEqual(try TeraPublicationRetry.decode(.deferredUntil(unixMs: 100)), .deferredUntil(100)) 25 XCTAssertEqual(try TeraPublicationRetry.decode(.inFlightUntil(unixMs: 100)), .inFlightUntil(100)) 26 // A native clock cannot substitute for a fresh Rust scheduling decision. 27 XCTAssertFalse(TeraPublicationRetry.deferredUntil(1).mayStart) 28 XCTAssertFalse(TeraPublicationRetry.inFlightUntil(1).mayStart) 29 } 30 31 func testMalformedWaitingBoundsFailClosed() { 32 for value in [UInt64(0), UInt64.max] { 33 XCTAssertThrowsError(try TeraPublicationRetry.decode(.deferredUntil(unixMs: value))) 34 XCTAssertThrowsError(try TeraPublicationRetry.decode(.inFlightUntil(unixMs: value))) 35 } 36 } 37 38 func testNeedsActionAndWaitDoNotAdvanceAStillQueuedSubmission() async throws { 39 let backend = AddBackend(advanceOffline: true) 40 let client = try await TeraAddStoreTests.startedClient(backend) 41 let store = TeraAddStore(runtimeClient: client) 42 await store.configure(snapshot: backend.snapshot()) 43 await store.start() 44 store.updateForm(\.content, "Saved original") 45 await store.submit() 46 let original = try XCTUnwrap(store.submissions.status) 47 XCTAssertEqual(original.state, .queued) 48 let before = await backend.submissionBackend.advanceCount 49 let decisions: [TeraPublicationRetry] = [ 50 .deferredUntil(1), .inFlightUntil(1), .needsAction(.deadlineExceeded), .needsAction(.attemptLimit), 51 .needsAction(.authenticationRequired), .needsAction(.quotaExceeded), 52 .needsAction(.invalidPayload), .needsAction(.deliveryRefused), 53 .needsAction(.coordinateChanged), 54 ] 55 for decision in decisions { 56 let status = TeraSubmissionStatus( 57 request: original.request, intentID: original.intentID, operationID: original.operationID, 58 revision: original.revision, captured: original.captured, state: original.state, 59 committedAtUnixMilliseconds: original.committedAtUnixMilliseconds, 60 updatedAtUnixMilliseconds: original.updatedAtUnixMilliseconds, 61 media: original.media, settlement: original.settlement, delivery: original.delivery, 62 targetDetails: original.targetDetails, retry: decision 63 ) 64 XCTAssertFalse(status.canOfferContinuation) 65 let effects = TeraSubmissionEffects(client: client, media: nil, ensure: {}, 66 accept: { _ in XCTFail("Blocked work cannot advance") }) 67 try await effects.advance(status) 68 XCTAssertEqual(status.captured, original.captured) 69 XCTAssertEqual(status.targetDetails, original.targetDetails) 70 } 71 let after = await backend.submissionBackend.advanceCount 72 XCTAssertEqual(after, before) 73 store.stop() 74 _ = try await client.stop() 75 } 76 }