field_ios

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

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 }