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

A new app's second change waits for the reviewer: the first-change rule applies once, proven by a test #12055

Merged opened this

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

Merged

mason/forge:mason/12048-a-new-app-s-second-change-waits-for-the → main 1 commit fork

  • Merged. mason/12048-a-new-app-s-second-change-waits-for-the was merged into main as 4571016.

Review

Sign in to review this change.
1 file changed +95 −0
test/first-change-once.test.ts added +95
@@ -0,0 +1,95 @@
1 /**
2 * The first-change rule applies once (#12048): after one crew pull of an app
3 * has merged, the next green crew pull waits for the reviewer under the normal
4 * rule. `nothingLandedYet` is read from merged pulls when the gate decides.
5 */
6 import { afterAll, beforeAll, expect, test } from "bun:test";
7 import { mkdtemp, rm } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { newApp } from "../src/actions/apps.ts";
11 import { addToCrew, createAgent } from "../src/actions/crew.ts";
12 import { openPull } from "../src/actions/pulls.ts";
13 import { commitToBranch } from "../src/actions/repos.ts";
14 import { createApp, type App } from "../src/app.ts";
15 import { getPull, type Pull } from "../src/core/pulls.ts";
16 import { getRepoById, type Repo } from "../src/core/repos.ts";
17 import { submitReview } from "../src/core/reviews.ts";
18 import { claimRun, createRun, finishRun } from "../src/core/runs.ts";
19 import { createUser, getUserByName, type User } from "../src/core/users.ts";
20 import { landIfGreen, reviewed } from "../src/crew/autonomy.ts";
21 import { isFirstChange, nothingLandedYet } from "../src/crew/first_change.ts";
22 import { seatAs } from "./helpers/seat.ts";
23 import { quietLogs } from "./helpers/globals.ts";
24
25 quietLogs();
26
27 let dir = "";
28 let app: App;
29 let alice: User;
30 let mason: User;
31 let quill: User;
32 let repo: Repo;
33 const fresh = () => getRepoById(app.db, repo.id)!;
34
35 async function greenPull(name: string): Promise<Pull> {
36 const branch = await commitToBranch(app, alice, repo, {
37 branch: `feature/${name}`,
38 base: "main",
39 message: name,
40 files: [{ path: `${name}.ts`, content: `export const n = "${name}";\n` }],
41 });
42 if (!branch.ok) throw new Error(JSON.stringify(branch.error));
43 const opened = await openPull(app, alice, repo, { title: name, baseBranch: "main", headBranch: `feature/${name}` });
44 if (!opened.ok) throw new Error(String(opened.error));
45 app.db.query("UPDATE issue SET author_id = ?2 WHERE id = ?1").run(opened.value.id, mason.id);
46 const pull = getPull(app.db, repo.id, opened.value.number)!;
47 const check = createRun(app.db, { repoId: repo.id, kind: "check", issueId: pull.id, env: `pull-${pull.number}`, commitOid: pull.headOid, reason: "pull" });
48 claimRun(app.db, check.id);
49 finishRun(app.db, check.id, { state: "ok", summary: "check passed" });
50 return pull;
51 }
52
53 beforeAll(async () => {
54 dir = await mkdtemp(join(tmpdir(), "forge-first-once-"));
55 app = createApp({ dataDir: dir, port: 0 });
56 const made = await createUser(app.db, { origin: "seed", trusted: true, username: "alice", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true });
57 if (!made.ok) throw new Error(made.error);
58 alice = made.value;
59 for (const name of ["mason", "quill"]) {
60 const agent = await createAgent(app, alice, { username: name });
61 if (!agent.ok) throw new Error(String(agent.error));
62 }
63 mason = getUserByName(app.db, "mason")!;
64 quill = getUserByName(app.db, "quill")!;
65 const created = await newApp(app, alice, { name: "hello", description: "A page that says hello", visibility: "private" });
66 if (!created.ok) throw new Error(JSON.stringify(created.error));
67 repo = created.value.repo;
68 addToCrew(app, alice, repo, "mason");
69 addToCrew(app, alice, repo, "quill");
70 seatAs(app.db, repo.id, mason.id, "builder");
71 seatAs(app.db, repo.id, quill.id, "reviewer");
72 app.db.query("UPDATE repo SET autonomy = 'autonomous', autonomy_by = ?2 WHERE id = ?1").run(repo.id, alice.id);
73 });
74
75 afterAll(async () => {
76 await app?.close?.();
77 await rm(dir, { recursive: true, force: true });
78 });
79
80 test("after one merged crew pull, a second green crew pull waits for the reviewer", async () => {
81 const first = await greenPull("first");
82 expect(nothingLandedYet(app.db, repo.id)).toBe(true);
83 expect(isFirstChange(app.db, fresh(), first)).toBe(true);
84 expect(await landIfGreen(app, fresh(), first)).toBe(true);
85
86 const second = await greenPull("second");
87 expect(nothingLandedYet(app.db, repo.id)).toBe(false);
88 expect(isFirstChange(app.db, fresh(), second)).toBe(false);
89 expect(await reviewed(app, fresh(), second)).toBe("hold");
90 expect(await landIfGreen(app, fresh(), second)).toBe(false);
91 expect(getPull(app.db, repo.id, second.number)?.mergedAt).toBeNull();
92
93 submitReview(app.db, { issueId: second.id, authorId: quill.id, state: "approved", commitOid: second.headOid, body: "Looks right." });
94 expect(await landIfGreen(app, fresh(), second)).toBe(true);
95 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs