eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1import XCTest
2@testable import LChatCore
3
4// 🤖 Unfurling your OWN just-posted message: the card used to appear only
5// after a reload. The row for a pending send is rebuilt the moment its echo
6// swaps the pending id for the real server id, and the rebuild cancels the old
7// row's `.task`. With the fetch parented to that task, the cancellation landed
8// as a FAILED unfurl, negative-cached the URL for a minute, and the
9// replacement row was told there was nothing to show.
10//
11// So the two rules pinned here: the fetch survives its caller's cancellation,
12// and a caller arriving mid-fetch awaits the same one instead of being told no.
13#if DEBUG
14final class UnfurlSharingTests: XCTestCase {
15 @MainActor
16 private func makeModel() -> ChatFeedModel {
17 let inst = SavedInstance(origin: "https://example.invalid", deviceToken: "tok",
18 screenName: "Paul", userPub: "", userPriv: "")
19 let thread = ChatThread(id: "t1", kind: "dm", title: "Eleven", preview: "",
20 lastAt: 0, unread: false, pinned: false, hidden: false,
21 members: [], threadKeyB64: "")
22 return ChatFeedModel(instance: inst, thread: thread, myUserId: "me")
23 }
24
25 private static let link = URL(string: "https://evilmartians.com")!
26 private static let card = LinkPreview(url: link, title: "Evil Martians",
27 description: "", imageURL: nil)
28
29 /// The row that replaces a cancelled one still gets its card.
30 @MainActor
31 func testFetchOutlivesACancelledCaller() async {
32 let model = makeModel()
33 let started = expectation(description: "fetch started")
34 let calls = Counter()
35 model._testUnfurlFetch = { _ in
36 calls.bump()
37 started.fulfill()
38 try? await Task.sleep(nanoseconds: 120_000_000)
39 return Self.card
40 }
41
42 // The pending row's task, cancelled the way a rebuilt row cancels it.
43 let row = Task { @MainActor in await model.unfurl(Self.link) }
44 await fulfillment(of: [started], timeout: 2)
45 row.cancel()
46
47 // The replacement row asks again while the first fetch is still out.
48 let replacement = await model.unfurl(Self.link)
49 XCTAssertEqual(replacement, Self.card,
50 "the rebuilt row must get the card the cancelled row started")
51 XCTAssertEqual(calls.value, 1, "one fetch, shared — not one per row")
52 XCTAssertEqual(model.cachedUnfurl(Self.link), .some(Self.card),
53 "and the cancellation must not have poisoned the cache")
54 }
55
56 /// Two rows asking at once both get the card — the second used to get nil
57 /// (the old in-flight guard) and cache that as "nothing to show".
58 @MainActor
59 func testConcurrentCallersShareOneFetch() async {
60 let model = makeModel()
61 let calls = Counter()
62 model._testUnfurlFetch = { _ in
63 calls.bump()
64 try? await Task.sleep(nanoseconds: 50_000_000)
65 return Self.card
66 }
67 async let a = model.unfurl(Self.link)
68 async let b = model.unfurl(Self.link)
69 let (first, second) = await (a, b)
70 XCTAssertEqual(first, Self.card)
71 XCTAssertEqual(second, Self.card)
72 XCTAssertEqual(calls.value, 1)
73 }
74
75 /// A page with nothing to show still fails closed, and briefly — the TTL
76 /// that keeps a re-render from stampeding the server.
77 @MainActor
78 func testEmptyResultIsNegativeCachedThenRetried() async {
79 let model = makeModel()
80 model._testUnfurlFetch = { _ in nil }
81 let empty = await model.unfurl(Self.link)
82 XCTAssertNil(empty)
83 XCTAssertEqual(model.cachedUnfurl(Self.link), .some(nil),
84 "known-empty, so height measurement doesn't leave a gap")
85 }
86}
87
88/// Call tally — MainActor like the model whose fetch it counts.
89@MainActor private final class Counter {
90 var value = 0
91 func bump() { value += 1 }
92}
93#endif
94