TeraTodayReconciliationTests.swift (6547B)
1 @testable import TeraApp 2 import XCTest 3 4 @MainActor 5 final class TeraTodayReconciliationTests: XCTestCase { 6 func testDraftStormRetainsLoadedOrderAndUnchangedCursor() async throws { 7 let backend = try TeraScopeBackend() 8 let (client, store) = try await makeStore(backend) 9 await store.loadNextPage() 10 let original = store.cards 11 let scope = store.scopeGeneration 12 let pause = await backend.pause(.reconcile) 13 await backend.emit(.drafts) 14 await pause.entered.wait() 15 for _ in 0 ..< 64 { 16 await backend.emit(.media) 17 await backend.emit(.drafts) 18 } 19 XCTAssertEqual(store.cards, original) 20 XCTAssertTrue(store.canLoadNextPage) 21 await pause.resume.open() 22 await TeraScopeFixtures.eventually { !store.presentation.isReading } 23 let requests = await backend.reconciliationRequests 24 XCTAssertTrue(requests.allSatisfy { $0.cardIDs.count <= 100 }) 25 XCTAssertEqual(store.cards.map(\.id), ["one", "two", "three"]) 26 XCTAssertEqual(store.currentCard(id: "two"), original[1]) 27 XCTAssertEqual(store.scopeGeneration, scope) 28 XCTAssertFalse(store.hasPendingContent) 29 XCTAssertTrue(store.canLoadNextPage) 30 store.stop() 31 _ = try await client.stop() 32 } 33 34 func testRemovedSelectedCardDisappearsAndNewContentWaitsForExplicitRefresh() async throws { 35 let backend = try TeraScopeBackend() 36 let (client, store) = try await makeStore(backend) 37 await store.loadNextPage() 38 let scope = store.scopeGeneration 39 await backend.setReconciliation(["new", "three", "one"].map(TeraScopeFixtures.card), generation: 2) 40 await backend.emit(.today) 41 await TeraScopeFixtures.eventually { store.hasPendingContent } 42 XCTAssertEqual(store.cards.map(\.id), ["one", "three"]) 43 XCTAssertNil(store.currentCard(id: "two")) 44 XCTAssertNotNil(store.currentCard(id: "three")) 45 XCTAssertEqual(store.scopeGeneration, scope) 46 XCTAssertFalse(store.canLoadNextPage) 47 await backend.setPage(page(["new", "one"], next: "fresh", generation: 2)) 48 await store.reload(refreshProjection: false) 49 XCTAssertEqual(store.cards.map(\.id), ["new", "one"]) 50 XCTAssertFalse(store.hasPendingContent) 51 XCTAssertTrue(store.canLoadNextPage) 52 store.stop() 53 _ = try await client.stop() 54 } 55 56 func testLateOldReconciliationCannotClearNewContext() async throws { 57 let backend = try TeraScopeBackend() 58 let (client, store) = try await makeStore(backend) 59 let pause = await backend.pause(.reconcile, fails: true) 60 await backend.emit(.today) 61 await pause.entered.wait() 62 store.configure(snapshot: TeraScopeFixtures.snapshot(account: "b", relay: "second")) 63 await backend.setPage(page(["new"], generation: 2)) 64 await store.reload(refreshProjection: false) 65 await pause.resume.open() 66 await TeraScopeFixtures.eventually { store.cards.map(\.id) == ["new"] } 67 XCTAssertNil(store.presentation.readFailure) 68 store.stop() 69 _ = try await client.stop() 70 } 71 72 func testMandatoryResnapshotFailureRemovesUnqualifiedContent() async throws { 73 let backend = try TeraScopeBackend() 74 let (client, store) = try await makeStore(backend) 75 let pause = await backend.pause(.reconcile, fails: true) 76 await backend.emit(.initial, delivery: .resnapshotRequired) 77 await pause.entered.wait() 78 await pause.resume.open() 79 await TeraScopeFixtures.eventually { store.hasPendingContent } 80 XCTAssertTrue(store.cards.isEmpty) 81 XCTAssertNil(store.currentCard(id: "one")) 82 XCTAssertFalse(store.canLoadNextPage) 83 XCTAssertNotNil(store.presentation.readFailure) 84 store.stop() 85 _ = try await client.stop() 86 } 87 88 func testVisibilityUpdateSupersedesAnOlderPageAlreadyInFlight() async throws { 89 let backend = try TeraScopeBackend() 90 let (client, store) = try await makeStore(backend) 91 let pause = await backend.pause(.page) 92 let paging = Task { await store.loadNextPage() } 93 await pause.entered.wait() 94 await backend.setReconciliation([TeraScopeFixtures.card("one")], generation: 2) 95 await backend.emit(.today) 96 await TeraScopeFixtures.eventually { store.hasPendingContent } 97 await pause.resume.open() 98 await paging.value 99 XCTAssertEqual(store.cards.map(\.id), ["one"]) 100 XCTAssertNil(store.currentCard(id: "two")) 101 XCTAssertFalse(store.canLoadNextPage) 102 store.stop() 103 _ = try await client.stop() 104 } 105 106 func testReconciliationDoesNotCombineChangedGenerationsAcrossBatches() async throws { 107 let backend = try TeraScopeBackend() 108 let client = try await TeraScopeFixtures.client(backend) 109 let cards = (0 ..< 101).map { TeraScopeFixtures.card(String($0)) } 110 await backend.setReconciliation(cards, generation: 1) 111 let pause = await backend.pause(.reconcile) 112 let read = Task { 113 try await TeraTodayReconciler.read( 114 client: client, context: TeraLocalNetwork.defaultContext(snapshot: TeraScopeFixtures.snapshot()), 115 calendar: TeraScopeFixtures.viewerCalendar(asOf: 1), cards: cards 116 ) 117 } 118 await pause.entered.wait() 119 await backend.setReconciliation(cards, generation: 2) 120 await pause.resume.open() 121 do { 122 _ = try await read.value 123 XCTFail("A mixed projection cannot become the current visible snapshot") 124 } catch { 125 let requests = await backend.reconciliationRequests 126 XCTAssertEqual(requests.map(\.cardIDs.count), [100, 1]) 127 XCTAssertEqual(requests.last?.expectedGeneration, 1) 128 } 129 _ = try await client.stop() 130 } 131 132 private func makeStore(_ backend: TeraScopeBackend) async throws -> (TeraRuntimeClient, TeraTodayStore) { 133 await backend.setPage(page(["one", "two"], next: "next")) 134 await backend.setPage(page(["three"], next: "last"), cursor: "next") 135 await backend.setReconciliation(["one", "two", "three"].map(TeraScopeFixtures.card), generation: 1) 136 let client = try await TeraScopeFixtures.client(backend) 137 let initial = await backend.pause(.reconcile) 138 let store = TeraTodayStore(runtimeClient: client, clock: .fixed(unixSeconds: 1)) 139 store.configure(snapshot: TeraScopeFixtures.snapshot()) 140 await store.start() 141 await initial.entered.wait() 142 await initial.resume.open() 143 await TeraScopeFixtures.eventually { store.cards.count == 2 && !store.presentation.isReading } 144 return (client, store) 145 } 146 147 private func page(_ ids: [String], next: String? = nil, generation: UInt64 = 1) -> TeraTodayPage { 148 TeraTodayPage(asOfUnixSeconds: 1, items: ids.map(TeraScopeFixtures.card), nextCursor: next, projectionGeneration: generation, calendar: TeraScopeFixtures.viewerCalendar(asOf: 1)) 149 } 150 }