field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

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 }