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

An open pull is always on the Pulls page: the tab's count and the list agree #10312

Merged opened this in answer to

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

Merged

scout/forge:scout/10293-an-open-pull-is-always-on-the-pulls-page → main 3 commits fork 2 threads · 2 unresolved

  • Merged. scout/10293-an-open-pull-is-always-on-the-pulls-page was merged into main as 3dd8c7b.

Review

Sign in to review this change.
2 files changed +77 −1
src/web/studio/lists.ts +2 −1
@@ -627,7 +627,8 @@ export async function changesList(ctx: Ctx): Promise<Response> {
627627 lap("labels");
628628 const rows = fetched
629629 .map((pull, i) => ({ pull, info: words[i]! }))
630 .filter((r) => shown(r.info.word, f));
630 // An open pull is always listed, whatever its word: the tab counts them all.
631 .filter((r) => f.word === undefined || shown(r.info.word, f));

Fine. Consider moving this rule into shown() itself (return true when f.word is undefined) so there is one place that decides what is listed, instead of a guard at the call site.

631632 const steps = stepsOfStored(db, repo, rows.map((r) => r.pull));
632633 lap("steps");
633634
test/pulls-page-open.test.ts added +75
@@ -0,0 +1,75 @@
1 /**
2 * An open pull is always on the Pulls page (#10293): a pull whose check was
3 * stopped is listed with its word, and the page's rows equal the open count.
4 */
5 import { afterAll, beforeAll, expect, test } from "bun:test";
6 import { mkdtemp, rm } from "node:fs/promises";
7 import { tmpdir } from "node:os";
8 import { join } from "node:path";
9 import { createRepo } from "../src/actions/repos.ts";
10 import { createApp, type App } from "../src/app.ts";
11 import { createIssue } from "../src/core/issues.ts";
12 import { createRun, finishRun } from "../src/core/runs.ts";
13 import { createUser } from "../src/core/users.ts";
14 import { serve, type Servers } from "../src/http/server.ts";
15
16 let dataDir: string;
17 let app: App;
18 let servers: Servers;
19 let cookie = "";
20
21 beforeAll(async () => {
22 dataDir = await mkdtemp(join(tmpdir(), "forge-pulls-open-"));
23 app = createApp({ dataDir, port: 0 });
24 servers = await serve(app);
25 const made = await createUser(app.db, {
26 origin: "seed", trusted: true, username: "alice", email: "[email protected]",
27 password: "a-good-passphrase", adminIfFirst: true,
28 });
29 if (!made.ok) throw new Error(made.error);
30 const repo = await createRepo(app, made.value, { name: "lib", visibility: "public" });
31 if (!repo.ok) throw new Error(String(repo.error));
32 const pull = (title: string, head: string) => {
33 const p = createIssue(app.db, { repoId: repo.value.id, authorId: made.value.id, title, kind: "pull" });
34 if (!p.ok) throw new Error(String(p.error));
35 app.db
36 .query(`INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, head_repo_id)
37 VALUES (?1, 'main', ?2, 'a', ?3, ?4)`)
38 .run(p.value.id, head, head, repo.value.id);
39 return p.value;
40 };
41 pull("Quiet one", "b".repeat(40));
42 const stopped = pull("Stopped one", "c".repeat(40));
43 const run = createRun(app.db, {
44 repoId: repo.value.id, kind: "check", issueId: stopped.id, commitOid: "c".repeat(40),
45 });
46 app.db.query("UPDATE run SET state = 'running' WHERE id = ?1").run(run.id);
47 finishRun(app.db, run.id, { state: "failed", summary: "bun test failed" });
48 const signin = await fetch(`${servers.url}/signin`, {
49 method: "POST",
50 headers: { origin: servers.url },
51 body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }),
52 redirect: "manual",
53 });
54 cookie = (signin.headers.get("set-cookie") ?? "").split(";")[0]!;
55 });
56
57 afterAll(async () => {
58 await servers.stop();
59 await rm(dataDir, { recursive: true, force: true });
60 });
61
62 test("a pull stopped on a failed check is listed, and rows equal the open count", async () => {
63 const res = await fetch(`${servers.url}/alice/lib/pulls`, { headers: { cookie } });
64 const text = await res.text();
65 expect(text).toContain("Stopped one");
66 expect(text).toContain("Quiet one");
67 const open = app.db
68 .query<{ n: number }, []>("SELECT COUNT(*) AS n FROM issue WHERE kind = 'pull' AND state = 'open'")
69 .get()!.n;
70 expect(open).toBe(2);
71 const rows = (text.match(/href="\/alice\/lib\/pulls\/\d+" class="row"/g) ?? []).length;
outdated

This assertion is a tautology: Stopped one was already asserted present, so split(...).length-1 >= 1 can never fail separately. And open is only compared with the literal 2, never with the page. Replace both with: count the page's pull rows and expect(rows).toBe(open); also assert the stopped row shows its word/reason, so the test fails if rows and count diverge.

72 expect(rows).toBe(open);
73 const stoppedRow = text.slice(text.indexOf("Stopped one"), text.indexOf("</li>", text.indexOf("Stopped one")));
74 expect(stoppedRow).toContain("Its check is red.");
75 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs