Commit ac81a4d179

ac81a4d179fae9ec7631b4587c5909352c532077

parent: 982ec60b32

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-07 02:22 UTC

Guard direct stub.seen indexing with #require

A raw stub.seen[n] crashes the whole test process the moment a
regression leaves fewer requests than expected, instead of failing
just that one test. Replace every unguarded index (and aliases of
stub.seen) with #require(stub.seen.dropFirst(n).first), which throws
a legible per-test failure instead.

Layout: unified · split

gitbayTests/BlameEditTests.swift +2 −1
@@ -64,7 +64,8 @@ struct BlameViewModelTests {
64 await model.loadMore() 64 await model.loadMore()
65 #expect(model.hunks.count == 2) 65 #expect(model.hunks.count == 2)
66 #expect(!model.hasMore) 66 #expect(!model.hasMore)
67 #expect(stub.seen[1].url.query()?.contains("argv=--from&argv=1001") == true) 67 let second = try #require(stub.seen.dropFirst().first)
68 #expect(second.url.query()?.contains("argv=--from&argv=1001") == true)
68 69
69 // Nothing further to ask for. 70 // Nothing further to ask for.
70 await model.loadMore() 71 await model.loadMore()
gitbayTests/ExploreRefLogTests.swift +2 −1
@@ -84,7 +84,8 @@ struct ExploreTests {
84 await list.loadMore() 84 await list.loadMore()
85 #expect(list.state.value?.count == 3) 85 #expect(list.state.value?.count == 3)
86 #expect(!list.hasMore) 86 #expect(!list.hasMore)
87 #expect(stub.seen[1].url.query()?.contains("argv=--cursor&argv=ZXhwbG9yZTpr") == true) 87 let second = try #require(stub.seen.dropFirst().first)
88 #expect(second.url.query()?.contains("argv=--cursor&argv=ZXhwbG9yZTpr") == true)
88 } 89 }
89 90
90 @Test func ownerAndNameSplitForDisplay() { 91 @Test func ownerAndNameSplitForDisplay() {
gitbayTests/GitbayClientTests.swift +6 −3
@@ -101,8 +101,10 @@ struct GitbayClientReadTests {
101 #expect(first[0].path == "krz/gitbay") 101 #expect(first[0].path == "krz/gitbay")
102 let requests = stub.seen 102 let requests = stub.seen
103 #expect(requests.count == 2) 103 #expect(requests.count == 2)
104 #expect(requests[0].headers["If-None-Match"] == nil) 104 let firstRequest = try #require(requests.first)
105 #expect(requests[1].headers["If-None-Match"] == etag) 105 #expect(firstRequest.headers["If-None-Match"] == nil)
106 let secondRequest = try #require(requests.dropFirst().first)
107 #expect(secondRequest.headers["If-None-Match"] == etag)
106 } 108 }
107 109
108 @Test func changedBodyReplacesTheCachedValidator() async throws { 110 @Test func changedBodyReplacesTheCachedValidator() async throws {
@@ -117,7 +119,8 @@ struct GitbayClientReadTests {
117 119
118 #expect(refreshed.isEmpty) 120 #expect(refreshed.isEmpty)
119 #expect(revalidated.isEmpty) 121 #expect(revalidated.isEmpty)
120 #expect(stub.seen[2].headers["If-None-Match"] == "\"bb\"") 122 let third = try #require(stub.seen.dropFirst(2).first)
123 #expect(third.headers["If-None-Match"] == "\"bb\"")
121 } 124 }
122 125
123 @Test func absentDataDecodesAsEmptyList() async throws { 126 @Test func absentDataDecodesAsEmptyList() async throws {
gitbayTests/MRViewModelTests.swift +6 −5
@@ -185,7 +185,8 @@ struct MRListViewModelTests {
185 } 185 }
186 186
187 #expect(stub.seen.count == 2) 187 #expect(stub.seen.count == 2)
188 #expect(stub.seen[1].url.query()?.contains("argv=merged") == true) 188 let second = try #require(stub.seen.dropFirst().first)
189 #expect(second.url.query()?.contains("argv=merged") == true)
189 guard case .empty = model.state else { 190 guard case .empty = model.state else {
190 Issue.record("expected .empty after filtering, got \(model.state)") 191 Issue.record("expected .empty after filtering, got \(model.state)")
191 return 192 return
@@ -264,7 +265,7 @@ struct MRDetailViewModelTests {
264 265
265 await model.review(.approve) 266 await model.review(.approve)
266 267
267 let write = stub.seen[3] 268 let write = try #require(stub.seen.dropFirst(3).first)
268 #expect(write.method == "POST") 269 #expect(write.method == "POST")
269 #expect(write.url.path() == "/api/v1/cmd") 270 #expect(write.url.path() == "/api/v1/cmd")
270 let body = try #require(try JSONSerialization.jsonObject(with: write.body) as? [String: Any]) 271 let body = try #require(try JSONSerialization.jsonObject(with: write.body) as? [String: Any])
@@ -282,7 +283,7 @@ struct MRDetailViewModelTests {
282 283
283 await model.comment("long review text\nwith lines") 284 await model.comment("long review text\nwith lines")
284 285
285 let write = stub.seen[3] 286 let write = try #require(stub.seen.dropFirst(3).first)
286 let body = try #require(try JSONSerialization.jsonObject(with: write.body) as? [String: Any]) 287 let body = try #require(try JSONSerialization.jsonObject(with: write.body) as? [String: Any])
287 #expect(body["argv"] as? [String] == ["mr", "comment", "krz/gitbay", "7", "--file", "-"]) 288 #expect(body["argv"] as? [String] == ["mr", "comment", "krz/gitbay", "7", "--file", "-"])
288 #expect(body["stdin"] as? String == "long review text\nwith lines") 289 #expect(body["stdin"] as? String == "long review text\nwith lines")
@@ -311,7 +312,7 @@ struct MRDetailViewModelTests {
311 312
312 await model.setResolved(thread, true) 313 await model.setResolved(thread, true)
313 314
314 let write = stub.seen[3] 315 let write = try #require(stub.seen.dropFirst(3).first)
315 let body = try #require(try JSONSerialization.jsonObject(with: write.body) as? [String: Any]) 316 let body = try #require(try JSONSerialization.jsonObject(with: write.body) as? [String: Any])
316 #expect(body["argv"] as? [String] == ["mr", "resolve", "krz/gitbay", "7", "3"]) 317 #expect(body["argv"] as? [String] == ["mr", "resolve", "krz/gitbay", "7", "3"])
317 } 318 }
@@ -326,7 +327,7 @@ struct MRDetailViewModelTests {
326 327
327 await model.reply(to: thread, "because 5xx is transient") 328 await model.reply(to: thread, "because 5xx is transient")
328 329
329 let write = stub.seen[3] 330 let write = try #require(stub.seen.dropFirst(3).first)
330 let body = try #require(try JSONSerialization.jsonObject(with: write.body) as? [String: Any]) 331 let body = try #require(try JSONSerialization.jsonObject(with: write.body) as? [String: Any])
331 #expect(body["argv"] as? [String] == 332 #expect(body["argv"] as? [String] ==
332 ["mr", "diff-comment", "krz/gitbay", "7", "--reply", "3", "--file", "-"]) 333 ["mr", "diff-comment", "krz/gitbay", "7", "--reply", "3", "--file", "-"])
gitbayTests/PagedListTests.swift +8 −4
@@ -38,9 +38,11 @@ struct ClientPagingTests {
38 #expect(first.next == "abc") 38 #expect(first.next == "abc")
39 #expect(second.items == [Row(number: 3)]) 39 #expect(second.items == [Row(number: 3)])
40 #expect(second.next == nil) 40 #expect(second.next == nil)
41 #expect(stub.seen[0].url.query() == 41 let firstSeen = try #require(stub.seen.first)
42 #expect(firstSeen.url.query() ==
42 "argv=issue&argv=list&argv=krz/gitbay&argv=--limit&argv=2") 43 "argv=issue&argv=list&argv=krz/gitbay&argv=--limit&argv=2")
43 #expect(stub.seen[1].url.query() == 44 let secondSeen = try #require(stub.seen.dropFirst().first)
45 #expect(secondSeen.url.query() ==
44 "argv=issue&argv=list&argv=krz/gitbay&argv=--limit&argv=2&argv=--cursor&argv=abc") 46 "argv=issue&argv=list&argv=krz/gitbay&argv=--limit&argv=2&argv=--cursor&argv=abc")
45 } 47 }
46} 48}
@@ -77,8 +79,10 @@ struct PagedListModelTests {
77 // Further calls are no-ops, not requests. 79 // Further calls are no-ops, not requests.
78 await list.loadMore() 80 await list.loadMore()
79 #expect(stub.seen.count == 3) 81 #expect(stub.seen.count == 3)
80 #expect(stub.seen[1].url.query()?.contains("argv=--cursor&argv=c1") == true) 82 let second = try #require(stub.seen.dropFirst().first)
81 #expect(stub.seen[2].url.query()?.contains("argv=--cursor&argv=c2") == true) 83 #expect(second.url.query()?.contains("argv=--cursor&argv=c1") == true)
84 let third = try #require(stub.seen.dropFirst(2).first)
85 #expect(third.url.query()?.contains("argv=--cursor&argv=c2") == true)
82 } 86 }
83 87
84 @Test func anEmptyFirstPageIsAnEmptyState() async throws { 88 @Test func anEmptyFirstPageIsAnEmptyState() async throws {
gitbayTests/ProfileCommitTests.swift +2 −1
@@ -59,7 +59,8 @@ struct ProfileAggregateTests {
59 #expect(profile.activityTotal == 15) 59 #expect(profile.activityTotal == 15)
60 // One read builds the whole page. 60 // One read builds the whole page.
61 #expect(stub.seen.count == 1) 61 #expect(stub.seen.count == 1)
62 #expect(stub.seen[0].url.query() == "argv=profile&argv=show&argv=cmc") 62 let first = try #require(stub.seen.first)
63 #expect(first.url.query() == "argv=profile&argv=show&argv=cmc")
63 } 64 }
64 65
65 @Test func anOrgProfileCarriesMembersInsteadOfOrgs() async throws { 66 @Test func anOrgProfileCarriesMembersInsteadOfOrgs() async throws {
gitbayTests/RefsMilestoneTests.swift +2 −1
@@ -120,7 +120,8 @@ struct MilestoneListViewModelTests {
120 } 120 }
121 121
122 #expect(stub.seen.count == 2) 122 #expect(stub.seen.count == 2)
123 #expect(stub.seen[1].url.query()?.contains("argv=closed") == true) 123 let second = try #require(stub.seen.dropFirst().first)
124 #expect(second.url.query()?.contains("argv=closed") == true)
124 } 125 }
125} 126}
126 127