TeraStoreRecoveryTests.swift (7103B)
1 @testable import TeraApp 2 import XCTest 3 4 @MainActor 5 final class TeraStoreRecoveryTests: XCTestCase { 6 func testRecoveredTodayAddAndMeReadCurrentStateWithoutAnotherEvent() async throws { 7 let backend = try TeraScopeBackend() 8 let client = try await TeraScopeFixtures.client(backend) 9 let retry = ResourceTestPause() 10 let today = TeraTodayStore(runtimeClient: client, observationDelay: { _ in await retry.wait() }) 11 let add = TeraAddStore(runtimeClient: client, observationDelay: { _ in await retry.wait() }) 12 let me = TeraMeStore(runtimeClient: client, observationDelay: { _ in await retry.wait() }) 13 configure(today: today, add: add, me: me) 14 await today.start() 15 await add.start() 16 await me.start() 17 await TeraScopeFixtures.eventually { 18 today.observationState == .active && add.observationState == .active && me.observationState == .active 19 } 20 await client.suspend() 21 await TeraScopeFixtures.eventually { 22 self.isRetrying(today.observationState) && self.isRetrying(add.observationState) && self.isRetrying(me.observationState) 23 } 24 await setCurrentState(backend, revision: 2) 25 await retry.resume.open() 26 await TeraScopeFixtures.eventually { 27 today.cards.isEmpty && today.hasPendingContent && add.drafts.first?.revision == 2 28 && me.snapshot?.cards.first?.id == "revision-2" && add.blossomEvidence?.observedAtUnixMilliseconds == 2 29 } 30 let subscriptions = await backend.counts[.subscribe] 31 XCTAssertEqual(subscriptions, 6) 32 today.stop() 33 add.stop() 34 me.stop() 35 _ = try await client.stop() 36 } 37 38 func testFinalGapRefreshesAllStoreDomainsWithoutOverwritingTheEditingForm() async throws { 39 let backend = try TeraScopeBackend() 40 let client = try await TeraScopeFixtures.client(backend) 41 let today = TeraTodayStore(runtimeClient: client) 42 let add = TeraAddStore(runtimeClient: client) 43 let me = TeraMeStore(runtimeClient: client) 44 configure(today: today, add: add, me: me) 45 await today.start() 46 await add.start() 47 await me.start() 48 await TeraScopeFixtures.eventually { 49 today.observationState == .active && add.observationState == .active && me.observationState == .active 50 } 51 add.updateForm(\.content, "Unsaved local editing") 52 await setCurrentState(backend, revision: 9) 53 await backend.emit(.initial, delivery: .resnapshotRequired) 54 await TeraScopeFixtures.eventually { 55 today.cards.isEmpty && today.hasPendingContent && add.drafts.first?.revision == 9 56 && me.snapshot?.cards.first?.id == "revision-9" && add.blossomEvidence?.observedAtUnixMilliseconds == 9 57 } 58 XCTAssertEqual(add.form.content, "Unsaved local editing") 59 let refreshes = await backend.counts[.refresh] 60 XCTAssertEqual(refreshes, 1, "Observation recovery reads local state without another network refresh") 61 today.stop() 62 add.stop() 63 me.stop() 64 _ = try await client.stop() 65 } 66 67 func testTodayStormDuringSlowReadReconcilesTheFinalDurableVisibility() async throws { 68 let backend = try TeraScopeBackend() 69 let client = try await TeraScopeFixtures.client(backend) 70 let store = TeraTodayStore(runtimeClient: client) 71 store.configure(snapshot: TeraScopeFixtures.snapshot()) 72 await store.start() 73 await TeraScopeFixtures.eventually { store.observationState == .active && !store.cards.isEmpty } 74 let first = await backend.pause(.reconcile) 75 await backend.emit(.today) 76 await first.entered.wait() 77 let before = await backend.counts[.reconcile, default: 0] 78 await setCurrentState(backend, revision: 256) 79 for _ in 1 ... 256 { 80 await backend.emit(.today) 81 } 82 let paused = await backend.counts[.reconcile, default: 0] 83 XCTAssertEqual(paused, before, "Only one observer query may execute at a time") 84 await first.resume.open() 85 await TeraScopeFixtures.eventually { store.cards.isEmpty && store.hasPendingContent } 86 XCTAssertEqual(store.presentation.content, .empty) 87 await store.reload(refreshProjection: false) 88 XCTAssertEqual(store.cards.first?.id, "revision-256") 89 store.stop() 90 _ = try await client.stop() 91 } 92 93 func testOrdinaryMediaProgressPreservesLoadedPagesAndTheNextCursor() async throws { 94 let backend = try TeraScopeBackend() 95 let client = try await TeraScopeFixtures.client(backend) 96 let subscription = await backend.pause(.subscribe) 97 let initial = await backend.pause(.reconcile) 98 await backend.setPage(page("one", next: "two")) 99 await backend.setPage(page("two", next: "three"), cursor: "two") 100 await backend.setPage(page("three"), cursor: "three") 101 let store = TeraTodayStore(runtimeClient: client) 102 store.configure(snapshot: TeraScopeFixtures.snapshot()) 103 await store.start() 104 // The mandatory initial resnapshot has the same visible identities. 105 await backend.setReconciliation(["one", "two", "three"].map(TeraScopeFixtures.card), generation: 1) 106 await subscription.resume.open() 107 await initial.entered.wait() 108 await initial.resume.open() 109 await TeraScopeFixtures.eventually { store.observationState == .active && !store.presentation.isReading } 110 await store.loadNextPage() 111 XCTAssertEqual(store.cards.map(\.id), ["one", "two"]) 112 let before = await backend.counts[.page, default: 0] 113 // Space hints beyond one actor turn so this exercises ordinary progress, 114 // independently of C029's intentionally stronger overflow resnapshot. 115 for _ in 1 ... 8 { 116 await backend.emit(.media) 117 try await Task.sleep(for: .milliseconds(10)) 118 } 119 let after = await backend.counts[.page, default: 0] 120 XCTAssertEqual(after, before) 121 XCTAssertEqual(store.cards.map(\.id), ["one", "two"]) 122 XCTAssertTrue(store.canLoadNextPage) 123 await store.loadNextPage() 124 XCTAssertEqual(store.cards.map(\.id), ["one", "two", "three"]) 125 store.stop() 126 _ = try await client.stop() 127 } 128 129 private func configure(today: TeraTodayStore, add: TeraAddStore, me: TeraMeStore) { 130 let snapshot = TeraScopeFixtures.snapshot() 131 today.configure(snapshot: snapshot) 132 add.configure(snapshot: snapshot) 133 me.configure(context: .defaultContext(snapshot: snapshot)) 134 } 135 136 private func setCurrentState(_ backend: TeraScopeBackend, revision: UInt64) async { 137 await backend.setPage(page("revision-\(revision)")) 138 await backend.setReconciliation([TeraScopeFixtures.card("revision-\(revision)")], generation: revision) 139 await backend.setDrafts([TeraScopeFixtures.draft("revision-\(revision)", revision: revision)]) 140 await backend.setMeCards([TeraScopeFixtures.card("revision-\(revision)")]) 141 await backend.configure(TeraScopeFixtures.snapshot(evidence: TeraScopeFixtures.evidence(observedAt: revision))) 142 } 143 144 private func page(_ id: String, next: String? = nil) -> TeraTodayPage { 145 TeraTodayPage(asOfUnixSeconds: 1, items: [TeraScopeFixtures.card(id)], nextCursor: next, projectionGeneration: 1, calendar: TeraScopeFixtures.viewerCalendar(asOf: 1)) 146 } 147 148 private func isRetrying(_ value: TeraRuntimeObservationState) -> Bool { 149 if case .retrying = value { 150 return true 151 } 152 return false 153 } 154 }