TeraPublicationStageTests.swift (9332B)
1 import Foundation 2 @testable import TeraApp 3 import XCTest 4 5 @MainActor 6 final class TeraPublicationStageTests: XCTestCase { 7 func testEveryDurablePhaseHasScopedStageAndEligibleActions() async throws { 8 let backend = AddBackend(advanceOffline: true) 9 let client = try await TeraAddStoreTests.startedClient(backend) 10 let store = TeraAddStore(runtimeClient: client) 11 await store.configure(snapshot: backend.snapshot()) 12 await store.start() 13 store.updateForm(\.content, "Saved original") 14 await store.submit() 15 let original = try XCTUnwrap(store.submissions.status) 16 let expected: [(TeraOutboxState, String)] = [ 17 (.draft, "Saved on this device."), (.mediaPreparing, "Preparing photo."), 18 (.mediaUploading, "Photo upload awaiting verification."), 19 (.readyToSign, "Awaiting signing."), (.signing, "Awaiting signing."), 20 (.signed, "Signed; local admission is pending."), (.queued, "Queued for the saved relays."), 21 (.delivering, "Sending to the saved relays."), 22 (.partiallyDelivered, "Partially delivered; review the saved relay outcomes."), 23 (.retryable, "Saved for retry."), (.terminal, "Publication needs attention."), 24 (.cancelled, "Local work stopped. Recorded remote effects are retained."), 25 (.complete, "Delivery completed for the saved relay policy."), 26 ] 27 XCTAssertEqual(Set(expected.map(\.0)), Set(TeraOutboxState.allCases)) 28 for (state, label) in expected { 29 let value = replacing(original, state: state) 30 XCTAssertEqual(value.summary, label) 31 XCTAssertEqual(state.summary(settlement: nil), label) 32 XCTAssertEqual(value.canOfferContinuation, ![.complete, .cancelled, .terminal].contains(state)) 33 XCTAssertEqual(value.canOfferStop, state.canCancel) 34 } 35 store.stop() 36 _ = try await client.stop() 37 } 38 39 func testLegacySatisfiedPlanDoesNotHideMixedOrStoppedEffects() { 40 let settlement = TeraOperationSettlement( 41 artifacts: 7, signed: 4, admitted: 3, pending: 1, retryable: 1, 42 indeterminate: 1, failedTerminal: 1, cancelled: 1, 43 deliveryPlans: 6, deliverySatisfied: 1, deliveryPending: 1, 44 deliveryRetryable: 1, deliveryExhausted: 1, deliveryFailedTerminal: 1, deliveryCancelled: 1 45 ) 46 let summary = TeraOutboxState.cancelled.summary(settlement: settlement) 47 for retained in ["Local work stopped", "1 of 6", "unknown", "pending", "saved for retry", 48 "limit was reached", "failure", "effects are retained", "4 signed", "3 admitted locally"] 49 { 50 XCTAssertTrue(summary.contains(retained), retained) 51 } 52 XCTAssertFalse(summary.contains("Delivery completed")) 53 XCTAssertFalse(summary.contains("Published")) 54 let draft = TeraDraftStatus( 55 id: "draft", revision: 1, authorPublicKey: "author", kind: .add, commandType: .createUpdate, 56 form: nil, state: .cancelled, cardID: "card", operationID: "operation", 57 createdAtUnixMilliseconds: 1, updatedAtUnixMilliseconds: 2, 58 media: [], settlement: settlement, isRevision: false 59 ) 60 XCTAssertEqual(draft.honestSummary, summary) 61 let legacy = TeraLegacyDraftSummary( 62 id: draft.id, revision: 1, kind: .add, commandType: .createUpdate, state: .cancelled, 63 hasForm: false, isRevision: false, createdAtUnixMilliseconds: 1, updatedAtUnixMilliseconds: 2, 64 mediaCount: 0, verifiedMediaCount: 0, possibleOrphanCount: 0, settlement: settlement 65 ) 66 XCTAssertEqual(legacy.honestSummary, summary) 67 } 68 69 func testMainSubmitAndAccessibleStatusHonorEverySavedHoldWithoutEffects() async throws { 70 let backend = AddBackend(advanceOffline: true) 71 let client = try await TeraAddStoreTests.startedClient(backend) 72 let store = TeraAddStore(runtimeClient: client) 73 await store.configure(snapshot: backend.snapshot()) 74 await store.start() 75 store.updateForm(\.content, "Saved original") 76 await store.submit() 77 let original = try XCTUnwrap(store.submissions.status) 78 let before = await backend.submissionBackend.advanceCount 79 let decisions: [(TeraPublicationRetry, String)] = [ 80 (.deferredUntil(1), "Publication waiting"), (.inFlightUntil(1), "Publication waiting"), 81 (.needsAction(.deadlineExceeded), "Publication needs attention"), 82 (.needsAction(.attemptLimit), "Publication needs attention"), 83 (.needsAction(.coordinateChanged), "Publication needs attention"), 84 (.needsAction(.authenticationRequired), "Publication needs attention"), 85 (.needsAction(.quotaExceeded), "Publication needs attention"), 86 (.needsAction(.invalidPayload), "Publication needs attention"), 87 (.needsAction(.deliveryRefused), "Publication needs attention"), 88 ] 89 for (retry, label) in decisions { 90 let value = replacing(original, retry: retry) 91 await backend.submissionBackend.installPresentationFixture(value) 92 await store.submissions.refreshSelected() 93 XCTAssertFalse(store.canSubmit) 94 XCTAssertFalse(store.submissions.canContinue) 95 XCTAssertEqual(store.submitLabel, label) 96 XCTAssertTrue(try store.submitAccessibilityValue.contains(XCTUnwrap(retry.explanation))) 97 await store.submit() 98 await store.submissions.continueSelected() 99 XCTAssertEqual(store.submissions.status?.request, original.request) 100 XCTAssertEqual(store.submissions.status?.captured, original.captured) 101 } 102 let after = await backend.submissionBackend.advanceCount 103 XCTAssertEqual(after, before) 104 await backend.submissionBackend.installPresentationFixture(original) 105 await store.submissions.refreshSelected() 106 XCTAssertTrue(store.canSubmit) 107 XCTAssertEqual(store.submitLabel, "Continue original submission") 108 store.stop() 109 _ = try await client.stop() 110 } 111 112 func testAcceptedCompletionAndStoppedEffectsRemainAccessibleWithoutRetry() async throws { 113 let backend = AddBackend() 114 let client = try await TeraAddStoreTests.startedClient(backend) 115 let store = TeraAddStore(runtimeClient: client) 116 await store.configure(snapshot: backend.snapshot()) 117 await store.start() 118 store.updateForm(\.content, "Saved original") 119 await store.submit() 120 XCTAssertEqual(store.submissions.status?.state, .complete) 121 XCTAssertFalse(store.canSubmit) 122 XCTAssertEqual(store.submitLabel, "Delivery policy complete") 123 XCTAssertTrue(store.submitAccessibilityValue.contains("saved relay policy")) 124 XCTAssertFalse(try XCTUnwrap(store.submissions.status).canOfferStop) 125 // A previously requested stop can arrive after acceptance; preserve both. 126 await store.submissions.requestStop() 127 XCTAssertFalse(store.canSubmit) 128 XCTAssertEqual(store.submitLabel, "Publication stopped") 129 XCTAssertTrue(store.submitAccessibilityValue.contains("Stopped")) 130 XCTAssertTrue(store.submitAccessibilityValue.contains("accepted")) 131 XCTAssertTrue(try XCTUnwrap(store.submissions.status).targetDetails.targets[0].accepted) 132 await store.submissions.refreshSelected() 133 XCTAssertTrue(store.submitAccessibilityValue.contains("accepted")) 134 store.stop() 135 _ = try await client.stop() 136 } 137 138 func testPartialUnknownAndStoppedFactsTakePrecedenceOverNominalStage() async throws { 139 let backend = AddBackend(advanceOffline: true) 140 let client = try await TeraAddStoreTests.startedClient(backend) 141 let store = TeraAddStore(runtimeClient: client) 142 await store.configure(snapshot: backend.snapshot()) 143 await store.start() 144 store.updateForm(\.content, "Saved original") 145 await store.submit() 146 let original = try XCTUnwrap(store.submissions.status) 147 for delivery in [TeraPublicationDeliveryState.unknown, .partiallyAccepted, .accepted] { 148 let evidence = TeraPublicationEvidence( 149 state: delivery, stopRequestedAtUnixMilliseconds: nil, schedulingRevision: 2, 150 retainedFacts: 1, recordedAttempts: 1, unresolvedClaims: true 151 ) 152 let current = replacing(original, state: .complete, delivery: evidence) 153 XCTAssertTrue(current.canOfferStop) 154 XCTAssertFalse(current.canOfferContinuation) 155 if delivery != .accepted { 156 XCTAssertFalse(current.summary.contains("completed")) 157 } 158 let stopped = TeraPublicationEvidence( 159 state: delivery, stopRequestedAtUnixMilliseconds: 10, schedulingRevision: 3, 160 retainedFacts: 1, recordedAttempts: 1, unresolvedClaims: true 161 ) 162 let value = replacing(original, state: .cancelled, delivery: stopped) 163 XCTAssertEqual(value.summary, stopped.stoppedSummary) 164 XCTAssertFalse(value.canOfferContinuation) 165 XCTAssertFalse(value.canOfferStop) 166 } 167 store.stop() 168 _ = try await client.stop() 169 } 170 171 private func replacing(_ original: TeraSubmissionStatus, state: TeraOutboxState = .queued, 172 retry: TeraPublicationRetry = .ready, delivery: TeraPublicationEvidence? = nil) -> TeraSubmissionStatus 173 { 174 TeraSubmissionStatus( 175 request: original.request, intentID: original.intentID, operationID: original.operationID, 176 revision: original.revision, captured: original.captured, state: state, 177 committedAtUnixMilliseconds: original.committedAtUnixMilliseconds, 178 updatedAtUnixMilliseconds: original.updatedAtUnixMilliseconds, 179 media: original.media, settlement: original.settlement, delivery: delivery ?? original.delivery, 180 targetDetails: original.targetDetails, retry: retry 181 ) 182 } 183 }