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

A test pins `reviewStats`: green crew pulls awaiting a verdict, the median wait for one, and verdicts per reviewer in the window #6708

Merged opened this

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

Merged

scout/forge:scout/6697-a-test-pins-reviewstats-green-crew-pulls → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6697-a-test-pins-reviewstats-green-crew-pulls was merged into main as 824c450.

Review

Sign in to review this change.
2 files changed +163 −2
src/core/review-stats.ts +2 −2
@@ -57,7 +57,7 @@ export function reviewStats(
5757 FROM review
5858 JOIN issue ON issue.id = review.issue_id
5959 JOIN user ON user.id = review.author_id AND user.is_agent = 1
60 WHERE issue.repo_id = ?1 AND review.created_at >= ?2`,
60 WHERE issue.repo_id = ?1 AND review.created_at >= ?2 AND review.state <> 'commented'`,

Correct. The same predicate is in the awaiting query and in byReviewer below; a shared constant would stop the three from drifting.

6161 )
6262 .all(repoId, since)
6363 .map((r) => r.ms)
@@ -67,7 +67,7 @@ export function reviewStats(
6767 `SELECT user.username AS name, COUNT(*) AS count FROM review
6868 JOIN issue ON issue.id = review.issue_id
6969 JOIN user ON user.id = review.author_id AND user.is_agent = 1
70 WHERE issue.repo_id = ?1 AND review.created_at >= ?2
70 WHERE issue.repo_id = ?1 AND review.created_at >= ?2 AND review.state <> 'commented'
7171 GROUP BY user.username ORDER BY count DESC, name`,
7272 )
7373 .all(repoId, since);
test/review-stats.test.ts added +161
@@ -0,0 +1,161 @@
1 /**
2 * `reviewStats` (#6697): the crew's green pulls awaiting a verdict, the median
3 * wait from a head's first passing check to a verdict, and verdicts per
4 * reviewer in the window — seeded as rows in a fresh database.
5 */
6 import { beforeEach, describe, expect, test } from "bun:test";
7 import type { Database } from "bun:sqlite";
8 import { reviewStats } from "../src/core/review-stats.ts";
9 import { migrations } from "../src/core/schema.ts";
10 import { openDb } from "../src/kernel/db.ts";
11
12 const MIN = 60_000;
13 const T0 = Date.parse("2026-10-20T12:00:00.000Z");
14 const at = (ms: number) => new Date(T0 + ms).toISOString();
15 const SINCE = at(-60 * MIN);
16 const oid = (c: string) => c.repeat(40);
17
18 let db: Database;
19 let n = 0;
20
21 beforeEach(() => {
22 db = openDb(":memory:", migrations);
23 db.run("PRAGMA foreign_keys = OFF");
24 n = 0;
25 db.run("INSERT INTO repo (id, owner_id, name) VALUES ('r', 'x', 'r')");
26 for (const [id, agent] of [["ann", 1], ["bo", 1], ["cy", 1], ["pat", 0]] as const)
27 db.query("INSERT INTO user (id, username, email, password_hash, is_agent) VALUES (?1, ?1, ?1 || '@x', 'h', ?2)").run(id, agent);
28 });
29
30 /** An open pull by `author` whose head is `head`; returns its issue id. */
31 function pull(author: string, head: string, o: { merged?: boolean; repo?: string } = {}): string {
32 const id = `i${++n}`;
33 db.query("INSERT INTO issue (id, repo_id, number, title, author_id, state) VALUES (?1, ?2, ?3, 't', ?4, 'open')").run(id, o.repo ?? "r", n, author);
34 db.query("INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, merged_at) VALUES (?1, 'main', 'b', ?2, ?3, ?4)").run(
35 id, oid("0"), head, o.merged ? at(0) : null,
36 );
37 return id;
38 }
39 function run(issue: string, head: string, state: string, finished: number, kind = "check") {
40 db.query("INSERT INTO run (id, repo_id, kind, issue_id, commit_oid, state, finished_at) VALUES (?1, 'r', ?2, ?3, ?4, ?5, ?6)").run(
41 `u${++n}`, kind, issue, head, state, at(finished),
42 );
43 }
44 function review(issue: string, head: string, by: string, state: string, when: number) {
45 db.query("INSERT INTO review (id, issue_id, author_id, state, commit_oid, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)").run(
46 `v${++n}`, issue, by, state, head, at(when),
47 );
48 }
49
50 describe("awaiting", () => {
51 test("counts an open agent pull with a green head and no verdict", () => {
52 const a = pull("ann", oid("a"));
53 run(a, oid("a"), "ok", 0);
54 expect(reviewStats(db, "r", SINCE).awaiting).toBe(1);
55 });
56
57 test("not a person's pull, a red or unchecked head, or a merged pull", () => {
58 const person = pull("pat", oid("a"));
59 run(person, oid("a"), "ok", 0);
60 const red = pull("ann", oid("b"));
61 run(red, oid("b"), "ok", 0);
62 run(red, oid("b"), "failed", 1);
63 const running = pull("ann", oid("c"));
64 run(running, oid("c"), "running", 0);
65 pull("ann", oid("d"));
66 const merged = pull("ann", oid("e"), { merged: true });
67 run(merged, oid("e"), "ok", 0);
68 expect(reviewStats(db, "r", SINCE).awaiting).toBe(0);
69 });
70
71 test("a verdict on the head takes it out; a comment does not", () => {
72 const a = pull("ann", oid("a"));
73 run(a, oid("a"), "ok", 0);
74 review(a, oid("a"), "bo", "commented", 5 * MIN);
75 expect(reviewStats(db, "r", SINCE).awaiting).toBe(1);
76 review(a, oid("a"), "bo", "approved", 6 * MIN);
77 expect(reviewStats(db, "r", SINCE).awaiting).toBe(0);
78 });
79
80 test("a verdict on an older head leaves the new green head waiting", () => {
81 const a = pull("ann", oid("b"));
82 run(a, oid("a"), "ok", 0);
83 review(a, oid("a"), "bo", "changes_requested", MIN);
84 run(a, oid("b"), "ok", 2 * MIN);
85 expect(reviewStats(db, "r", SINCE).awaiting).toBe(1);
86 });
87
88 test("a deploy run counts like a check, and another repository's pulls do not", () => {
89 const a = pull("ann", oid("a"));
90 run(a, oid("a"), "ok", 0, "deploy");
91 db.run("INSERT INTO repo (id, owner_id, name) VALUES ('s', 'x', 's')");
92 const other = pull("ann", oid("b"), { repo: "s" });
93 run(other, oid("b"), "ok", 0);
94 expect(reviewStats(db, "r", SINCE).awaiting).toBe(1);
95 });
96 });
97
98 describe("latencyMs", () => {
99 test("null with no verdict in the window", () => {
100 expect(reviewStats(db, "r", SINCE).latencyMs).toBeNull();
101 const a = pull("ann", oid("a"));
102 run(a, oid("a"), "ok", -200 * MIN);
103 review(a, oid("a"), "bo", "approved", -100 * MIN);
104 expect(reviewStats(db, "r", SINCE).latencyMs).toBeNull();
105 });
106
107 test("odd count: the middle wait, from the first passing check", () => {
108 for (const [wait, head] of [[10, "a"], [30, "b"], [20, "c"]] as const) {
109 const p = pull("ann", oid(head));
110 run(p, oid(head), "failed", -50 * MIN);
111 run(p, oid(head), "ok", -40 * MIN);
112 run(p, oid(head), "ok", -30 * MIN); // a later pass does not move the start
113 review(p, oid(head), "bo", "approved", -40 * MIN + wait * MIN);
114 }
115 expect(reviewStats(db, "r", SINCE).latencyMs).toBeCloseTo(20 * MIN, -2);
116 });
117
118 test("even count: the mean of the two middle waits", () => {
119 for (const [wait, head] of [[10, "a"], [20, "b"], [30, "c"], [100, "d"]] as const) {
120 const p = pull("ann", oid(head));
121 run(p, oid(head), "ok", -50 * MIN);
122 review(p, oid(head), "bo", "approved", -50 * MIN + wait * MIN);
123 }
124 expect(reviewStats(db, "r", SINCE).latencyMs).toBeCloseTo(25 * MIN, -2);
125 });
126
127 test("a person's review, a comment, and a review with no passing check are not waits", () => {
128 const a = pull("ann", oid("a"));
129 run(a, oid("a"), "ok", -50 * MIN);
130 review(a, oid("a"), "pat", "approved", -10 * MIN);
131 review(a, oid("a"), "bo", "commented", -20 * MIN);
132 const b = pull("ann", oid("b"));
133 run(b, oid("b"), "failed", -50 * MIN);
134 review(b, oid("b"), "bo", "approved", -10 * MIN);
135 expect(reviewStats(db, "r", SINCE).latencyMs).toBeNull();
136 });
137 });
138
139 describe("byReviewer", () => {
140 test("counts verdicts since `since`, most first, ties by name, never a person's", () => {
141 const a = pull("ann", oid("a"));
142 run(a, oid("a"), "ok", -50 * MIN);
143 review(a, oid("a"), "cy", "approved", -40 * MIN);
144 review(a, oid("a"), "bo", "changes_requested", -30 * MIN);
145 review(a, oid("a"), "bo", "approved", -20 * MIN);
146 review(a, oid("a"), "ann", "approved", -10 * MIN);
147 review(a, oid("a"), "pat", "approved", -10 * MIN);
148 review(a, oid("a"), "cy", "approved", -300 * MIN); // before the window
149 expect(reviewStats(db, "r", SINCE).byReviewer).toEqual([
150 { name: "bo", count: 2 },
151 { name: "ann", count: 1 },
152 { name: "cy", count: 1 },
153 ]);
154 });
155
156 test("a comment is not a verdict", () => {
157 const a = pull("ann", oid("a"));
158 review(a, oid("a"), "bo", "commented", -10 * MIN);
159 expect(reviewStats(db, "r", SINCE).byReviewer).toEqual([]);
160 });
161 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs