TeraTodayBackfillTests.swift (6294B)
1 @testable import TeraApp 2 import XCTest 3 4 @MainActor 5 final class TeraTodayBackfillTests: XCTestCase { 6 func testExplicitBackfillPreservesCacheAndIncompleteHistoryAcrossLocalReloads() async throws { 7 let session = try await prepared() 8 let backend = session.backend 9 let client = session.client 10 let store = session.store 11 let cached = store.cards 12 await store.reload(refreshProjection: false) 13 XCTAssertEqual(store.discovery.continuation, "older") 14 let initialCalls = await backend.counts[.refresh] 15 XCTAssertEqual(initialCalls, 1) 16 17 await backend.setSyncReceipt(receipt(nil, incomplete: true)) 18 let pause = await backend.pause(.refresh) 19 let search = Task { await store.searchOlderPosts() } 20 await pause.entered.wait() 21 XCTAssertTrue(store.discovery.isSearching) 22 XCTAssertFalse(store.discovery.canSearchOlder) 23 XCTAssertEqual(store.cards, cached) 24 await store.searchOlderPosts() 25 await store.reload(refreshProjection: false) 26 XCTAssertTrue(store.discovery.isSearching) 27 XCTAssertEqual(store.discovery.continuation, "older") 28 let cursor = await backend.lastBackfillCursor 29 let calls = await backend.counts[.refresh] 30 XCTAssertEqual(cursor, "older") 31 XCTAssertEqual(calls, 2) 32 await pause.resume.open() 33 await search.value 34 XCTAssertFalse(store.discovery.isSearching) 35 XCTAssertNil(store.discovery.continuation) 36 XCTAssertTrue(store.discovery.hadIncompleteResponses) 37 XCTAssertEqual(store.discovery.message, "Some relay responses were incomplete. Older searches may leave gaps.") 38 XCTAssertEqual(store.presentation.relayReceipt, receipt(nil, incomplete: true)) 39 await backend.setSyncReceipt(receipt("new", incomplete: false)) 40 await store.reload() 41 XCTAssertEqual(store.discovery.continuation, "new") 42 XCTAssertFalse(store.discovery.hadIncompleteResponses) 43 let ordinaryCursor = await backend.lastBackfillCursor 44 XCTAssertNil(ordinaryCursor) 45 _ = try await client.stop() 46 } 47 48 func testLateOldContextBackfillCannotRestoreContinuationOrCards() async throws { 49 let session = try await prepared() 50 let backend = session.backend 51 let client = session.client 52 let store = session.store 53 await backend.setSyncReceipt(receipt("old-result", incomplete: true)) 54 let pause = await backend.pause(.refresh) 55 let search = Task { await store.searchOlderPosts() } 56 await pause.entered.wait() 57 store.configure(snapshot: TeraScopeFixtures.snapshot(relay: "second")) 58 XCTAssertNil(store.discovery.continuation) 59 await backend.setSyncReceipt(receipt("new-context", incomplete: false)) 60 await store.reload() 61 await pause.resume.open() 62 await search.value 63 XCTAssertEqual(store.discovery.continuation, "new-context") 64 XCTAssertFalse(store.discovery.hadIncompleteResponses) 65 XCTAssertEqual(store.cards.map(\.id), ["wss://second.example"]) 66 _ = try await client.stop() 67 } 68 69 func testCancelledBackfillKeepsRetryCursorAndIgnoresLateCompletion() async throws { 70 let session = try await prepared() 71 let backend = session.backend 72 let client = session.client 73 let store = session.store 74 let cached = store.cards 75 await backend.setSyncReceipt(receipt("late", incomplete: false)) 76 let pause = await backend.pause(.refresh) 77 let search = Task { await store.searchOlderPosts() } 78 await pause.entered.wait() 79 search.cancel() 80 await search.value 81 XCTAssertFalse(store.discovery.isSearching) 82 XCTAssertTrue(store.discovery.canSearchOlder) 83 await pause.resume.open() 84 XCTAssertEqual(store.discovery.continuation, "older") 85 XCTAssertTrue(store.discovery.hadIncompleteResponses) 86 XCTAssertEqual(store.cards, cached) 87 _ = try await client.stop() 88 } 89 90 func testFailureKeepsRetryButStaleCursorRequiresNewSearch() async throws { 91 let session = try await prepared() 92 let backend = session.backend 93 let client = session.client 94 let store = session.store 95 let cached = store.cards 96 for stale in [false, true] { 97 let failure: TeraRuntimeFailure = stale 98 ? .local(operation: "test", code: "today_cursor_invalid", safeMessage: "Controlled stale search.") 99 : TeraScopeFixtures.failure() 100 let pause = await backend.pause(.refresh, failure: failure) 101 let search = Task { await store.searchOlderPosts() } 102 await pause.entered.wait() 103 await pause.resume.open() 104 await search.value 105 XCTAssertEqual(store.discovery.failure?.requiresRefresh, stale) 106 XCTAssertEqual(store.discovery.canSearchOlder, !stale) 107 XCTAssertEqual(store.discovery.continuation, stale ? nil : "older") 108 XCTAssertEqual(store.cards, cached) 109 } 110 _ = try await client.stop() 111 } 112 113 func testNewOrdinaryRefreshInvalidatesPendingOlderSearch() async throws { 114 let session = try await prepared() 115 let backend = session.backend 116 let client = session.client 117 let store = session.store 118 await backend.setSyncReceipt(receipt("late", incomplete: true)) 119 let pause = await backend.pause(.refresh) 120 let search = Task { await store.searchOlderPosts() } 121 await pause.entered.wait() 122 await backend.setSyncReceipt(receipt("new", incomplete: false)) 123 await store.reload() 124 await pause.resume.open() 125 await search.value 126 XCTAssertEqual(store.discovery.continuation, "new") 127 XCTAssertFalse(store.discovery.hadIncompleteResponses) 128 XCTAssertFalse(store.discovery.isSearching) 129 _ = try await client.stop() 130 } 131 132 private struct Session { 133 let backend: TeraScopeBackend 134 let client: TeraRuntimeClient 135 let store: TeraTodayStore 136 } 137 138 private func prepared() async throws -> Session { 139 let backend = try TeraScopeBackend() 140 await backend.setSyncReceipt(receipt("older", incomplete: true)) 141 let client = try await TeraScopeFixtures.client(backend) 142 let store = TeraTodayStore(runtimeClient: client, clock: .fixed(unixSeconds: 1)) 143 store.configure(snapshot: TeraScopeFixtures.snapshot()) 144 await store.reload() 145 return Session(backend: backend, client: client, store: store) 146 } 147 148 private func receipt(_ cursor: String?, incomplete: Bool) -> TeraTodaySyncReceipt { 149 TeraTodaySyncFixtures.receipt( 150 discovery: TeraTodayDiscoveryReceipt(continuation: cursor, hadIncompleteResponses: incomplete) 151 ) 152 } 153 }