| 1 | import XCTest |
| 2 | @testable import LChatCore |
| 3 | |
| 4 | // 🤖 The refresh gate's contract: overlapping calls collapse into exactly |
| 5 | // one trailing rerun (not zero — a frame that arrives mid-pass must still |
| 6 | // produce a fresh pass; not N — the stampede was the bug); sequential |
| 7 | // calls each run; and a mid-pass caller resumes only after a pass that |
| 8 | // STARTED after its call (createChat reads `threads` right after run()). |
| 9 | @MainActor |
| 10 | final class CoalescedRunnerTests: XCTestCase { |
| 11 | func testSequentialCallsEachRun() async { |
| 12 | let runner = CoalescedRunner() |
| 13 | var passes = 0 |
| 14 | await runner.run { passes += 1 } |
| 15 | await runner.run { passes += 1 } |
| 16 | XCTAssertEqual(passes, 2) |
| 17 | } |
| 18 | |
| 19 | func testOverlappingCallsCollapseToOneTrailingRerunAndAwaitIt() async { |
| 20 | let runner = CoalescedRunner() |
| 21 | var passes = 0 |
| 22 | var blocked = false |
| 23 | var release: CheckedContinuation<Void, Never>? |
| 24 | // First pass blocks inside the runner so the overlapping calls below |
| 25 | // genuinely arrive mid-pass; the trailing rerun (same closure) must |
| 26 | // not block again. |
| 27 | let first = Task { @MainActor in |
| 28 | await runner.run { |
| 29 | passes += 1 |
| 30 | if !blocked { |
| 31 | blocked = true |
| 32 | await withCheckedContinuation { release = $0 } |
| 33 | } |
| 34 | } |
| 35 | } |
| 36 | while release == nil { await Task.yield() } |
| 37 | // Three callers while the pass is running: none may run a pass of |
| 38 | // their own, and none may RESUME until the trailing rerun has |
| 39 | // covered them. |
| 40 | var resumed = 0 |
| 41 | let waiters = (0..<3).map { _ in |
| 42 | Task { @MainActor in |
| 43 | await runner.run { passes += 1 } |
| 44 | resumed += 1 |
| 45 | } |
| 46 | } |
| 47 | for _ in 0..<20 { await Task.yield() } // let the waiters park in run() |
| 48 | XCTAssertEqual(passes, 1, "mid-pass calls must not run a pass themselves") |
| 49 | XCTAssertEqual(resumed, 0, "mid-pass callers wait for their covering pass") |
| 50 | release?.resume() |
| 51 | _ = await first.value |
| 52 | for w in waiters { _ = await w.value } |
| 53 | XCTAssertEqual(passes, 2, "exactly one trailing rerun after the pass ends") |
| 54 | XCTAssertEqual(resumed, 3, "every waiter resumed once the rerun landed") |
| 55 | } |
| 56 | |
| 57 | func testCallAfterCompletionRunsFresh() async { |
| 58 | // The current-task handle must be cleared by the time any awaiter |
| 59 | // resumes — a later call must start a fresh pass, never attach to a |
| 60 | // finished task and return passless. |
| 61 | let runner = CoalescedRunner() |
| 62 | var passes = 0 |
| 63 | await runner.run { passes += 1 } |
| 64 | await runner.run { passes += 1 } |
| 65 | await runner.run { passes += 1 } |
| 66 | XCTAssertEqual(passes, 3) |
| 67 | } |
| 68 | } |
| 69 | |