Public: anyone can read this; the crew can change it.

A pure read says, per repository, how many approved pulls wait for the merge queue and how long the oldest has waited #6563

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6552-a-pure-read-says-per-repository-how-many → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6552-a-pure-read-says-per-repository-how-many was merged into main as 97deca9.

Review

Sign in to review this change.
2 files changed +83 −0
src/flow/approved_wait.ts added +47
@@ -0,0 +1,47 @@
1 /**
2 * The approved wait per repository (#6552, for #5930): how many approved pulls
3 * wait for the merge queue and how long the oldest has waited. Pure: rows in,
4 * rows out; `now` is passed in.
5 */
6
7 const HOUR_MS = 3_600_000;
8
9 export interface ApprovedRow {
10 readonly repo: string;
11 readonly number: number;
12 readonly approvedAt: string;
13 }
14
15 export interface ApprovedWait {
16 readonly repo: string;
17 readonly waiting: number;
18 /** Hours the oldest has waited, one decimal; a future approval reads 0. */
19 readonly oldestHours: number;
20 /** The number of the pull that has waited longest. */
21 readonly oldest: number;
22 }
23
24 export function approvedWait(rows: readonly ApprovedRow[], now: Date): ApprovedWait[] {
25 const by = new Map<string, { waiting: number; at: number; number: number }>();
26 for (const row of rows) {
27 const at = Date.parse(row.approvedAt);
28 if (Number.isNaN(at)) continue;

The tie-break (same time, lower number wins) and the NaN skip on line 27 have no test. Add one each.

29 const cur = by.get(row.repo);
30 if (!cur) by.set(row.repo, { waiting: 1, at, number: row.number });
31 else {
32 cur.waiting += 1;
33 if (at < cur.at || (at === cur.at && row.number < cur.number)) {
34 cur.at = at;
35 cur.number = row.number;
36 }
37 }
38 }
39 return [...by]
40 .map(([repo, v]) => ({
41 repo,
42 waiting: v.waiting,
43 oldestHours: Math.round(Math.max(0, (now.getTime() - v.at) / HOUR_MS) * 10) / 10,
44 oldest: v.number,
45 }))
46 .sort((a, b) => b.oldestHours - a.oldestHours || b.waiting - a.waiting || a.repo.localeCompare(b.repo));
47 }
test/flow-approved-wait.test.ts added +36
@@ -0,0 +1,36 @@
1 import { expect, test } from "bun:test";
2 import { approvedWait } from "../src/flow/approved_wait.ts";
3
4 const now = new Date("2026-10-26T12:00:00Z");
5 const ago = (h: number) => new Date(now.getTime() - h * 3_600_000).toISOString();
6
7 test("one repository counts its pulls and names the oldest", () => {
8 const out = approvedWait([{ repo: "a", number: 1, approvedAt: ago(1) }, { repo: "a", number: 2, approvedAt: ago(5) }], now);
9 expect(out).toEqual([{ repo: "a", waiting: 2, oldestHours: 5, oldest: 2 }]);
10 });
11
12 test("several repositories are separate, longest wait first", () => {
13 const out = approvedWait(
14 [
15 { repo: "a", number: 1, approvedAt: ago(2) },
16 { repo: "b", number: 7, approvedAt: ago(30) },
17 { repo: "c", number: 3, approvedAt: ago(10) },
18 ],
19 now,
20 );
21 expect(out.map((r) => r.repo)).toEqual(["b", "c", "a"]);
22 });
23
24 test("nothing approved reads empty", () => {
25 expect(approvedWait([], now)).toEqual([]);
26 });
27
28 test("hours round to one decimal", () => {
29 const out = approvedWait([{ repo: "a", number: 1, approvedAt: ago(1.26) }], now);
30 expect(out[0]!.oldestHours).toBe(1.3);
31 });
32
33 test("an approval in the future reads 0", () => {
34 const out = approvedWait([{ repo: "a", number: 1, approvedAt: ago(-3) }], now);
35 expect(out[0]!.oldestHours).toBe(0);
36 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs