1import {
2 type PullRequestCheck,
3 PullRequestComment,
4 ThreadPullRequestWatch,
5} from "@t3tools/contracts";
6import { assert, describe, it } from "@effect/vitest";
7β
8import {
9 PULL_REQUEST_WATCH_WAKE_LIMIT,
10 evaluatePullRequestWatch as evaluate,
11 pullRequestWatchMessage,
12} from "./pullRequestWatch.ts";
13β
14// These cases read the head once or at one moment, so the no-checks notice never comes due.
15const evaluatePullRequestWatch = (
16 ...args: Parameters<typeof evaluate> extends [...infer Head, number] ? Head : never
17) => evaluate(...args, 0);
18β
19const STARTED = "2026-10-02T12:00:00.000Z";
20β
21const watch = (overrides: Partial<ThreadPullRequestWatch> = {}): ThreadPullRequestWatch => ({
22 startedAt: STARTED,
23 headSha: null,
24 failedChecks: [],
25 passed: false,
26 passedChecks: [],
27 remarksThrough: STARTED,
28 remarkIds: [],
29 conflicting: false,
30 wakes: 0,
31 ...overrides,
32});
33β
34const run = (id: number) => `https://github.com/o/r/actions/runs/${id}`;
35β
36const check = (name: string, status: PullRequestCheck["status"]): PullRequestCheck => ({
37 name,
38 status,
39 description: null,
40 url: `https://ci.example/${name}`,
41});
42β
43type Detail = Parameters<typeof evaluatePullRequestWatch>[1];
44β
45const capabilities = (checksAlwaysReport: boolean): Detail["capabilities"] => ({
46 diff: true,
47 comment: true,
48 actions: [],
49 mergeMethods: [],
50 search: false,
51 review: { inlineComment: false, reply: false, resolve: false, verdicts: [] },
52 reviewers: { request: false, listCandidates: false },
53 checksAlwaysReport,
54});
55β
56const detail = (overrides: Partial<Detail> = {}): Detail => ({
57 capabilities: capabilities(true),
58 headSha: "aaaaaaaaaa",
59 checks: [check("lint", "success"), check("test", "pending")],
60 mergeability: "mergeable",
61 viewer: "agent-user",
62 author: { login: "agent-user", name: null, avatarUrl: null },
63 ...overrides,
64});
65β
66const remark = (
67 login: string,
68 createdAt: string,
69 body = "Please rename this.",
70): PullRequestComment => ({
71 id: `${login}-${createdAt}`,
72 kind: "review-comment",
73 author: { login, name: null, avatarUrl: null },
74 body,
75 createdAt,
76 url: `https://github.com/o/r/pull/1#${login}`,
77 path: "src/index.ts",
78 reviewState: null,
79});
80β
81const noRemarks: ReadonlyArray<PullRequestComment> = [];
82β
83describe("evaluatePullRequestWatch", () => {
84 it("waits for workflow approval once, through reads that miss it, until the run finishes", () => {
85 const bot = check("bot", "success");
86 const approval = { ...check("CI", "action-required"), url: run(1) };
87 const gated = detail({ checks: [bot, approval] });
88 const first = evaluatePullRequestWatch(watch(), gated, noRemarks);
89 assert.deepEqual(first.changes, [{ kind: "checks-awaiting-approval", checks: [approval] }]);
90β
91 // GitHub's approval read can answer empty while the rest of the checks read fine.
92 const missing = evaluatePullRequestWatch(first.next, detail({ checks: [bot] }), noRemarks);
93 assert.deepEqual(missing.changes, []);
94 assert.isFalse(missing.next.passed);
95 assert.deepEqual(evaluatePullRequestWatch(missing.next, gated, noRemarks).changes, []);
96 const later = { ...check("Preview", "action-required"), url: run(2) };
97 assert.deepEqual(
98 evaluatePullRequestWatch(missing.next, detail({ checks: [bot, approval, later] }), noRemarks)
99 .changes,
100 [],
101 );
102 const unnamed = { ...check("Docs", "action-required"), url: null };
103 const named = detail({ checks: [bot, unnamed] });
104 const alone = evaluatePullRequestWatch(watch(), named, noRemarks);
105 assert.deepEqual(alone.changes, [{ kind: "checks-awaiting-approval", checks: [unnamed] }]);
106 const placeholder = { ...unnamed, name: "Workflow approval status", placeholder: true };
107 const unlisted = evaluatePullRequestWatch(
108 alone.next,
109 detail({ checks: [bot, placeholder] }),
110 noRemarks,
111 );
112 assert.deepEqual(unlisted.changes, []);
113 assert.isFalse(unlisted.next.passed);
114 assert.deepEqual(evaluatePullRequestWatch(unlisted.next, named, noRemarks).changes, []);
115 const dropped = evaluatePullRequestWatch(alone.next, detail({ checks: [bot] }), noRemarks);
116 assert.deepEqual(dropped.changes, []);
117 assert.isFalse(dropped.next.passed);
118 assert.deepEqual(dropped.next.awaitingApproval, [unnamed.name]);
119 assert.deepEqual(evaluatePullRequestWatch(dropped.next, named, noRemarks).changes, []);
120 const namedPending = evaluatePullRequestWatch(
121 dropped.next,
122 detail({
123 checks: [
124 { ...bot, required: true },
125 { ...unnamed, status: "pending" },
126 ],
127 }),
128 noRemarks,
129 );
130 assert.deepEqual(namedPending.changes, []);
131 assert.isFalse(namedPending.next.passed);
132 const namedDone = detail({ checks: [bot, { ...unnamed, status: "success" }] });
133 const namedFinished = evaluatePullRequestWatch(namedPending.next, namedDone, noRemarks);
134 assert.deepEqual(namedFinished.changes, [{ kind: "checks-passed", count: 2, required: false }]);
135 assert.deepEqual(namedFinished.next.awaitingApproval, []);
136 assert.deepEqual(
137 evaluatePullRequestWatch(namedFinished.next, namedDone, noRemarks).changes,
138 [],
139 );
140 const pushed = detail({ headSha: "bbbbbbbbbb", checks: [bot] });
141 assert.deepEqual(evaluatePullRequestWatch(missing.next, pushed, noRemarks).changes, [
142 { kind: "checks-passed", count: 1, required: false },
143 ]);
144β
145 // Approved, the run's jobs carry their own names under the run's URL.
146 const job = { ...check("lint", "success"), url: `${run(1)}/job/7` };
147 const pending = { ...check("test", "pending"), url: `${run(1)}/job/8` };
148 const required = { ...bot, required: true };
149 const running = evaluatePullRequestWatch(
150 missing.next,
151 detail({ checks: [required, job, pending] }),
152 noRemarks,
153 );
154 assert.deepEqual(running.changes, []);
155 assert.isFalse(running.next.passed);
156 assert.deepEqual(running.next.awaitingApproval, [run(1)]);
157 const overlap = evaluatePullRequestWatch(
158 running.next,
159 detail({ checks: [required, job, { ...pending, url: `${run(2)}/job/9` }] }),
160 noRemarks,
161 );
162 assert.deepEqual(overlap.changes, []);
163 assert.isFalse(overlap.next.passed);
164 assert.deepEqual(overlap.next.awaitingApproval, [run(1)]);
165 const unlinked = evaluatePullRequestWatch(
166 overlap.next,
167 detail({ checks: [required, job, { ...pending, url: null }] }),
168 noRemarks,
169 );
170 assert.deepEqual(unlinked.changes, []);
171 assert.isFalse(unlinked.next.passed);
172 assert.deepEqual(unlinked.next.awaitingApproval, [run(1)]);
173 const advisory = evaluatePullRequestWatch(
174 unlinked.next,
175 detail({ checks: [required, job, { ...pending, url: "https://ci.example/test" }] }),
176 noRemarks,
177 );
178 assert.deepEqual(advisory.changes, [{ kind: "checks-passed", count: 1, required: true }]);
179 assert.deepEqual(advisory.next.awaitingApproval, []);
180 const completed = detail({ checks: [required, job, { ...pending, status: "success" }] });
181 const finished = evaluatePullRequestWatch(unlinked.next, completed, noRemarks);
182 assert.deepEqual(finished.changes, [{ kind: "checks-passed", count: 1, required: true }]);
183 assert.deepEqual(finished.next.awaitingApproval, []);
184 assert.deepEqual(evaluatePullRequestWatch(finished.next, completed, noRemarks).changes, []);
185β
186 const done = evaluatePullRequestWatch(missing.next, detail({ checks: [bot, job] }), noRemarks);
187 assert.deepEqual(done.changes, [{ kind: "checks-passed", count: 2, required: false }]);
188 });
189β
190 it("does not pass or fail while the host cannot tell whether workflows await approval", () => {
191 const bot = check("bot", "success");
192 const unknown = {
193 ...check("Workflow approval status", "action-required"),
194 url: null,
195 placeholder: true,
196 };
197 const unread = evaluatePullRequestWatch(watch(), detail({ checks: [bot, unknown] }), noRemarks);
198 assert.deepEqual(unread.changes, []);
199 assert.isFalse(unread.next.passed);
200 const read = evaluatePullRequestWatch(unread.next, detail({ checks: [bot] }), noRemarks);
201 assert.deepEqual(read.changes, [{ kind: "checks-passed", count: 1, required: false }]);
202 const blocked = evaluatePullRequestWatch(
203 read.next,
204 detail({ checks: [{ ...bot, status: "pending" }, unknown] }),
205 noRemarks,
206 );
207 assert.isFalse(blocked.next.passed);
208 assert.deepEqual(
209 evaluatePullRequestWatch(blocked.next, detail({ checks: [bot] }), noRemarks).changes,
210 [{ kind: "checks-passed", count: 1, required: false }],
211 );
212 });
213β
214 it("reports each failure at once, even while another check never finishes", () => {
215 const bot = check("CodeRabbit", "pending");
216 const first = detail({ checks: [check("lint", "failure"), check("test", "pending"), bot] });
217 const lint = evaluatePullRequestWatch(watch(), first, noRemarks);
218 assert.deepEqual(lint.changes, [{ kind: "checks-failed", failed: [check("lint", "failure")] }]);
219 assert.deepEqual(evaluatePullRequestWatch(lint.next, first, noRemarks).changes, []);
220β
221 const second = detail({ checks: [check("lint", "failure"), check("test", "failure"), bot] });
222 const test = evaluatePullRequestWatch(lint.next, second, noRemarks);
223 assert.deepEqual(test.changes, [{ kind: "checks-failed", failed: [check("test", "failure")] }]);
224β
225 const rerun = evaluatePullRequestWatch(test.next, first, noRemarks);
226 assert.equal(evaluatePullRequestWatch(rerun.next, second, noRemarks).changes.length, 1);
227β
228 const pushed = detail({ ...second, headSha: "bbbbbbbbbb" });
229 assert.equal(evaluatePullRequestWatch(test.next, pushed, noRemarks).changes.length, 1);
230β
231 const approval = { ...check("CI", "action-required"), url: run(1) };
232 const gated = detail({ checks: [approval, check("lint", "failure")] });
233 assert.deepEqual(evaluatePullRequestWatch(watch(), gated, noRemarks).changes, [
234 { kind: "checks-failed", failed: [check("lint", "failure")] },
235 { kind: "checks-awaiting-approval", checks: [approval] },
236 ]);
237 });
238β
239 it("reports passed once the required checks pass, whatever the others do", () => {
240 const required = (name: string, status: PullRequestCheck["status"]) => ({
241 ...check(name, status),
242 required: true,
243 });
244 const green = detail({
245 checks: [required("test", "success"), required("lint", "success"), check("bot", "pending")],
246 });
247 const passed = evaluatePullRequestWatch(watch(), green, noRemarks);
248 assert.deepEqual(passed.changes, [{ kind: "checks-passed", count: 2, required: true }]);
249 assert.deepEqual(evaluatePullRequestWatch(passed.next, green, noRemarks).changes, []);
250β
251 const plain = detail({ checks: [check("test", "success"), check("bot", "pending")] });
252 assert.deepEqual(evaluatePullRequestWatch(watch(), plain, noRemarks).changes, []);
253 });
254β
255 it("reports passed again when a required check shows up already passed", () => {
256 const required = (name: string, status: PullRequestCheck["status"]) => ({
257 ...check(name, status),
258 required: true,
259 });
260 const tests = detail({
261 checks: [required("Tests", "success"), check("Smoke Tests", "pending")],
262 });
263 const first = evaluatePullRequestWatch(watch(), tests, noRemarks);
264 assert.deepEqual(first.changes, [{ kind: "checks-passed", count: 1, required: true }]);
265β
266 const gated = detail({
267 checks: [
268 required("Tests", "success"),
269 check("Smoke Tests", "success"),
270 required("Smoke Tests Gate", "success"),
271 ],
272 });
273 const second = evaluatePullRequestWatch(first.next, gated, noRemarks);
274 assert.deepEqual(second.changes, [{ kind: "checks-passed", count: 2, required: true }]);
275 assert.deepEqual(evaluatePullRequestWatch(second.next, gated, noRemarks).changes, []);
276β
277 const pending = detail({
278 checks: [required("Tests", "success"), required("Smoke Tests Gate", "pending")],
279 });
280 const waiting = evaluatePullRequestWatch(first.next, pending, noRemarks);
281 assert.deepEqual(waiting.changes, []);
282 assert.deepEqual(evaluatePullRequestWatch(waiting.next, gated, noRemarks).changes, [
283 { kind: "checks-passed", count: 2, required: true },
284 ]);
285 });
286β
287 // regression: #23712 required checks woke the agent early and the rest finishing never did
288 it("wakes again when the other checks finish after the required ones passed", () => {
289 const required = (name: string, status: PullRequestCheck["status"]) => ({
290 ...check(name, status),
291 required: true,
292 });
293 const early = detail({
294 checks: [required("test", "success"), check("e2e", "pending")],
295 });
296 const first = evaluatePullRequestWatch(watch(), early, noRemarks);
297 assert.deepEqual(first.changes, [{ kind: "checks-passed", count: 1, required: true }]);
298β
299 const done = detail({
300 checks: [required("test", "success"), check("e2e", "success")],
301 });
302 const final = evaluatePullRequestWatch(first.next, done, noRemarks);
303 assert.deepEqual(
304 final.changes.map((change) => change.kind),
305 ["checks-passed"],
306 );
307 assert.deepEqual(evaluatePullRequestWatch(final.next, done, noRemarks).changes, []);
308 // A later advisory job running and passing is not more news.
309 const botPending = detail({ checks: [...done.checks, check("bot", "pending")] });
310 const churn = evaluatePullRequestWatch(final.next, botPending, noRemarks);
311 assert.deepEqual(churn.changes, []);
312 const botDone = detail({ checks: [...done.checks, check("bot", "success")] });
313 assert.deepEqual(evaluatePullRequestWatch(churn.next, botDone, noRemarks).changes, []);
314β
315 // A pending check that turns required on the next read still finishes with one wake.
316 const promoted = detail({
317 checks: [required("test", "success"), required("e2e", "success")],
318 });
319 const gated = evaluatePullRequestWatch(first.next, promoted, noRemarks);
320 assert.deepEqual(gated.changes, [{ kind: "checks-passed", count: 2, required: true }]);
321 assert.deepEqual(evaluatePullRequestWatch(gated.next, promoted, noRemarks).changes, []);
322β
323 // Nothing else was pending when the required ones passed, so nothing more is owed.
324 const green = evaluatePullRequestWatch(watch(), done, noRemarks);
325 assert.deepEqual(green.changes, [{ kind: "checks-passed", count: 1, required: true }]);
326 assert.deepEqual(evaluatePullRequestWatch(green.next, botDone, noRemarks).changes, []);
327 });
328β
329 it("reports an owed non-required failure once after the required notice", () => {
330 const required = { ...check("test", "success"), required: true };
331 const early = detail({ checks: [required, check("e2e", "pending")] });
332 const first = evaluatePullRequestWatch(watch(), early, noRemarks);
333 assert.deepEqual(first.changes, [{ kind: "checks-passed", count: 1, required: true }]);
334 const failed = detail({ checks: [required, check("e2e", "failure")] });
335 const failure = evaluatePullRequestWatch(first.next, failed, noRemarks);
336 assert.deepEqual(failure.changes, [
337 { kind: "checks-failed", failed: [check("e2e", "failure")] },
338 ]);
339 const repeated = evaluatePullRequestWatch(failure.next, failed, noRemarks);
340 assert.deepEqual(repeated.changes, []);
341 const recovered = detail({ checks: [required, check("e2e", "success")] });
342 const final = evaluatePullRequestWatch(repeated.next, recovered, noRemarks);
343 assert.deepEqual(final.changes, [{ kind: "checks-passed", count: 2, required: false }]);
344 assert.deepEqual(evaluatePullRequestWatch(final.next, recovered, noRemarks).changes, []);
345 });
346β
347 it("clears old-head owed checks when a new head is pushed", () => {
348 const required = { ...check("test", "success"), required: true };
349 const early = detail({ checks: [required, check("e2e", "pending")] });
350 const first = evaluatePullRequestWatch(watch(), early, noRemarks);
351 assert.deepEqual(first.next.owedChecks, ["e2e"]);
352 const unreadHead = detail({ headSha: "bbbbbbbbbb", checks: [] });
353 const unread = evaluatePullRequestWatch(first.next, unreadHead, noRemarks);
354 assert.deepEqual(unread.changes, []);
355 assert.deepEqual(unread.next.owedChecks, []);
356 const pushed = detail({ ...unreadHead, checks: [required] });
357 const newHead = evaluatePullRequestWatch(unread.next, pushed, noRemarks);
358 assert.deepEqual(newHead.changes, [{ kind: "checks-passed", count: 1, required: true }]);
359 assert.deepEqual(newHead.next.owedChecks, []);
360 const lateResult = detail({ ...pushed, checks: [required, check("e2e", "success")] });
361 const final = evaluatePullRequestWatch(newHead.next, lateResult, noRemarks);
362 assert.deepEqual(final.changes, []);
363 assert.deepEqual(evaluatePullRequestWatch(final.next, lateResult, noRemarks).changes, []);
364 });
365β
366 // regression: #16208 PR watches burned GitHub's rate limit and gave up
367 it("does not report passed again for a new passed check where none is required", () => {
368 const first = evaluatePullRequestWatch(
369 watch(),
370 detail({ checks: [check("test", "success")] }),
371 noRemarks,
372 );
373 assert.deepEqual(first.changes, [{ kind: "checks-passed", count: 1, required: false }]);
374 const both = detail({ checks: [check("test", "success"), check("lint", "success")] });
375 assert.deepEqual(evaluatePullRequestWatch(first.next, both, noRemarks).changes, []);
376 });
377β
378 it("does not wake a watch saved before passed checks were recorded", () => {
379 const green = detail({ checks: [{ ...check("test", "success"), required: true }] });
380 const told = watch({ headSha: "aaaaaaaaaa", passed: true });
381 const saved = evaluatePullRequestWatch(told, green, noRemarks);
382 assert.deepEqual(saved.changes, []);
383 assert.deepEqual(saved.next.passedChecks, ["test"]);
384 });
385β
386 it("keeps remarks for a later pass when the conversation was not read whole", () => {
387 const comments = [remark("reviewer", "2026-10-02T12:06:00Z")];
388 const partial = evaluatePullRequestWatch(watch(), detail(), null);
389 assert.deepEqual(partial.changes, []);
390 assert.equal(
391 evaluatePullRequestWatch(partial.next, detail(), comments).changes[0]?.kind,
392 "remarks",
393 );
394 });
395β
396 it("reports a remark that shows up late with the same time as a reported one", () => {
397 const first = remark("reviewer", "2026-10-02T12:06:00Z");
398 const late = { ...remark("bot", "2026-10-02T12:06:00Z"), id: "late" };
399 const reported = evaluatePullRequestWatch(watch(), detail(), [first]);
400 const again = evaluatePullRequestWatch(reported.next, detail(), [first, late]);
401 assert.deepEqual(again.changes, [{ kind: "remarks", remarks: [late] }]);
402 assert.deepEqual(again.next.remarkIds, [first.id, "late"]);
403 });
404β
405 it("reports edits after the watermark once and counts them toward the wake limit", () => {
406 const old = remark("greptile[bot]", "2026-10-02T11:00:00Z");
407 const watching = watch({
408 headSha: "aaaaaaaaaa",
409 remarkIds: [old.id],
410 wakes: PULL_REQUEST_WATCH_WAKE_LIMIT - 1,
411 });
412 assert.deepEqual(evaluatePullRequestWatch(watching, detail(), [old]).changes, []);
413 const edited = { ...old, editedAt: "2026-10-02T12:06:00Z" };
414 const report = evaluatePullRequestWatch(watching, detail(), [edited]);
415 assert.deepEqual(report.changes, [{ kind: "remarks", remarks: [edited] }]);
416 assert.equal(report.next.remarksThrough, edited.editedAt);
417 assert.deepEqual(report.next.remarkIds, [old.id]);
418 assert.isTrue(report.exhausted);
419 assert.deepEqual(evaluatePullRequestWatch(report.next, detail(), [edited]).changes, []);
420 const late = { ...edited, id: "late" };
421 assert.deepEqual(evaluatePullRequestWatch(report.next, detail(), [edited, late]).changes, [
422 { kind: "remarks", remarks: [late] },
423 ]);
424 });
425β
426 it("wakes once per edit of a remark seen at the watermark, never for the agent's own", () => {
427 const seen = remark("ci-bot", "2026-10-10T19:52:40.000Z");
428 const watching = watch({
429 headSha: "aaaaaaaaaa",
430 remarksThrough: seen.createdAt,
431 remarkIds: [seen.id],
432 wakes: 2,
433 });
434 assert.deepEqual(evaluatePullRequestWatch(watching, detail(), [seen]).changes, []);
435 const edited = { ...seen, editedAt: "2026-10-10T20:04:50.000Z" };
436 const first = evaluatePullRequestWatch(watching, detail(), [edited]);
437 assert.deepEqual(first.changes, [{ kind: "remarks", remarks: [edited] }]);
438 assert.strictEqual(first.next.remarksThrough, edited.editedAt);
439 assert.deepEqual(evaluatePullRequestWatch(first.next, detail(), [edited]).changes, []);
440 const again = { ...seen, editedAt: "2026-10-10T20:05:50.000Z" };
441 const second = evaluatePullRequestWatch(first.next, detail(), [again]);
442 assert.deepEqual(second.changes, [{ kind: "remarks", remarks: [again] }]);
443 assert.deepEqual(evaluatePullRequestWatch(second.next, detail(), [again]).changes, []);
444 const own = { ...edited, author: { login: "agent-user", name: null, avatarUrl: null } };
445 assert.deepEqual(evaluatePullRequestWatch(watching, detail(), [own]).changes, []);
446 });
447β
448 it("does not treat a failed check read as a rerun", () => {
449 const failed = detail({ checks: [check("lint", "failure")] });
450 const reported = evaluatePullRequestWatch(watch(), failed, noRemarks);
451 assert.equal(reported.changes.length, 1);
452 const unreadable = evaluatePullRequestWatch(reported.next, detail({ checks: [] }), noRemarks);
453 assert.deepEqual(evaluatePullRequestWatch(unreadable.next, failed, noRemarks).changes, []);
454 });
455β
456 it("wakes for the pull request's author when the agent is someone else", () => {
457 const contributor = detail({ author: { login: "contributor", name: null, avatarUrl: null } });
458 const reply = remark("contributor", "2026-10-02T12:06:00Z");
459 assert.deepEqual(evaluatePullRequestWatch(watch(), contributor, [reply]).changes, [
460 { kind: "remarks", remarks: [reply] },
461 ]);
462 const noViewer = detail({ viewer: undefined, author: contributor.author });
463 assert.deepEqual(evaluatePullRequestWatch(watch(), noViewer, [reply]).changes, []);
464 });
465β
466 it("reports remarks from others once and never the agent's own", () => {
467 const comments = [
468 remark("agent-user", "2026-10-02T12:05:00Z", "Fixed in the latest push."),
469 remark("macroscope-app[bot]", "2026-10-02T12:06:00Z"),
470 remark("reviewer", "2026-10-02T11:00:00Z", "Older than the watch."),
471 ];
472 const report = evaluatePullRequestWatch(watch(), detail(), comments);
473 assert.deepEqual(report.changes, [{ kind: "remarks", remarks: [comments[1]!] }]);
474 assert.equal(report.next.remarksThrough, "2026-10-02T12:06:00Z");
475 assert.deepEqual(evaluatePullRequestWatch(report.next, detail(), comments).changes, []);
476 });
477β
478 it("cuts a long remark in the wake message without splitting an emoji", () => {
479 const body = `${"x".repeat(196)}π and more`;
480 const report = evaluatePullRequestWatch(watch(), detail(), [
481 remark("reviewer", "2026-10-02T12:06:00Z", body),
482 ]);
483 const { text } = pullRequestWatchMessage({
484 number: 1,
485 url: "https://github.com/o/r/pull/1",
486 baseBranch: "main",
487 headSha: null,
488 report,
489 });
490 assert.isTrue(text.isWellFormed());
491 assert.include(text, `"${"x".repeat(196)}..."`);
492 });
493β
494 it("reports a conflict once, until the branch is clean again", () => {
495 const conflicting = detail({ mergeability: "conflicting" });
496 const first = evaluatePullRequestWatch(watch(), conflicting, noRemarks);
497 assert.deepEqual(first.changes, [{ kind: "conflicting" }]);
498 const { headSha: _unreadHead, ...headUnread } = conflicting;
499 const unread = evaluatePullRequestWatch(first.next, headUnread, noRemarks);
500 assert.deepEqual(unread.changes, []);
501 assert.deepEqual(evaluatePullRequestWatch(unread.next, conflicting, noRemarks).changes, []);
502 const recomputing = evaluatePullRequestWatch(
503 first.next,
504 detail({ mergeability: "unknown" }),
505 noRemarks,
506 );
507 assert.deepEqual(
508 evaluatePullRequestWatch(recomputing.next, conflicting, noRemarks).changes,
509 [],
510 );
511 const clean = evaluatePullRequestWatch(first.next, detail(), noRemarks);
512 assert.deepEqual(evaluatePullRequestWatch(clean.next, conflicting, noRemarks).changes, [
513 { kind: "conflicting" },
514 ]);
515 const pushed = { headSha: "bbbbbbbbbb" };
516 const recomputed = evaluatePullRequestWatch(
517 first.next,
518 detail({ ...pushed, mergeability: "unknown" }),
519 noRemarks,
520 );
521 assert.deepEqual(
522 evaluatePullRequestWatch(
523 recomputed.next,
524 detail({ ...pushed, mergeability: "conflicting" }),
525 noRemarks,
526 ).changes,
527 [{ kind: "conflicting" }],
528 );
529 });
530β
531 it("does not spend the comment wake limit on check results", () => {
532 const tired = watch({ headSha: "aaaaaaaaaa", wakes: PULL_REQUEST_WATCH_WAKE_LIMIT - 1 });
533 const result = evaluatePullRequestWatch(
534 tired,
535 detail({ checks: [check("lint", "failure")] }),
536 noRemarks,
537 );
538 assert.isFalse(result.exhausted);
539 assert.equal(result.next.wakes, 0);
540 });
541β
542 it("stops after the wake limit unless the head moves", () => {
543 const comments = [remark("reviewer", "2026-10-02T12:10:00Z")];
544 const tired = watch({ headSha: "aaaaaaaaaa", wakes: PULL_REQUEST_WATCH_WAKE_LIMIT - 1 });
545 assert.isTrue(evaluatePullRequestWatch(tired, detail(), comments).exhausted);
546 const pushed = evaluatePullRequestWatch(tired, detail({ headSha: "cccccccccc" }), comments);
547 assert.isFalse(pushed.exhausted);
548 assert.equal(pushed.next.wakes, 1);
549 });
550β
551 const anHourLater = (
552 first: ThreadPullRequestWatch,
553 read: Detail,
554 ): ReadonlyArray<ReadonlyArray<string>> => {
555 const kinds: Array<ReadonlyArray<string>> = [];
556 let current = first;
557 for (let minute = 0; minute <= 60; minute += 10) {
558 const { changes, next } = evaluate(current, read, noRemarks, minute * 60_000);
559 kinds.push(changes.map((change) => change.kind));
560 current = next;
561 }
562 return kinds;
563 };
564β
565 it("does not tell a watch saved before the no-checks clock that its seen head has none", () => {
566 // Such a watch may have seen this head's checks pending, which records nothing.
567 const legacy = watch({ headSha: "aaaaaaaaaa" });
568 assert.isTrue(anHourLater(legacy, detail({ checks: [] })).every((kinds) => kinds.length === 0));
569 const pushed = anHourLater(legacy, detail({ headSha: "bbbbbbbbbb", checks: [] }));
570 assert.deepEqual(pushed.flat(), ["no-checks"]);
571 const unknown = detail({
572 checks: [{ ...check("Workflow approval status", "action-required"), placeholder: true }],
573 });
574 assert.deepEqual(anHourLater(watch(), unknown).flat(), []);
575 const unread = evaluate(watch(), unknown, noRemarks, 0);
576 assert.deepEqual(
577 evaluate(unread.next, detail({ checks: [] }), noRemarks, 60 * 60_000).changes,
578 [{ kind: "no-checks", minutes: 60 }],
579 );
580 });
581β
582 it("never reports missing checks on a host where an empty list says nothing", () => {
583 const silent = detail({ capabilities: capabilities(false), checks: [] });
584 assert.isTrue(anHourLater(watch(), silent).every((kinds) => kinds.length === 0));
585 });
586});