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

A test pins leave-to-run: a grant names one head, a new push waits for a new grant, and a grant is never a review #6724

Merged opened this

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

Merged

scout/forge:scout/6718-a-test-pins-leave-to-run-a-grant-names-o → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6718-a-test-pins-leave-to-run-a-grant-names-o was merged into main as 7bafffa.

Review

Sign in to review this change.
2 files changed +141 −0
src/core/run_grant.ts +1
@@ -11,6 +11,7 @@ export function grantRun(
1111 commitOid: string,
1212 userId: string,
1313 ): void {
14 if (commitOid === "") return;

Good: the writer now matches the reader's empty-oid rule, so no row is ever written that runGrantedAt would have to hide.

1415 db.query(
1516 `INSERT INTO pull_run_grant (issue_id, commit_oid, user_id) VALUES (?1, ?2, ?3)
1617 ON CONFLICT (issue_id, commit_oid) DO UPDATE SET
test/run-grant.test.ts added +140
@@ -0,0 +1,140 @@
1 /**
2 * Leave to run: a grant names one head, a new push waits for a new grant,
3 * and a grant is never a review.
4 */
5 import { afterAll, beforeAll, describe, 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 { mergePull, openPull } from "../src/actions/pulls.ts";
10 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
11 import { createApp, type App } from "../src/app.ts";
12 import { recordPullProposal } from "../src/core/proposal_token.ts";
13 import { pullMayRun } from "../src/core/pull_trust.ts";
14 import { getPull } from "../src/core/pulls.ts";
15 import type { Repo } from "../src/core/repos.ts";
16 import { grantRun, runGrantedAt } from "../src/core/run_grant.ts";
17 import { createUser, type User } from "../src/core/users.ts";
18
19 describe("leave to run", () => {
20 let dataDir: string;
21 let app: App;
22 let alice: User;
23 let lib: Repo;
24 let issueId: string;
25 let number: number;
26 let a: string;
27 let b: string;
28 const pull = () => getPull(app.db, lib.id, number)!;
29 const rows = () =>
30 app.db
31 .query<{ user_id: string; granted_at: string }, [string]>(
32 "SELECT user_id, granted_at FROM pull_run_grant WHERE issue_id = ?1",
33 )
34 .all(issueId);
35
36 const push = async (file: string) => {
37 const done = await commitToBranch(app, alice, lib, {
38 branch: "child/kid/one",
39 message: file,
40 files: [{ path: file, content: `${file}\n` }],
41 });
42 if (!done.ok) throw new Error(JSON.stringify(done.error));
43 return done.value.commitOid;
44 };
45
46 beforeAll(async () => {
47 dataDir = await mkdtemp(join(tmpdir(), "forge-run-grant-"));
48 app = createApp({ dataDir, port: 0, host: "forge.test" });
49 const made = await createUser(app.db, {
50 origin: "seed",
51 trusted: true,
52 username: "alice",
53 email: "[email protected]",
54 password: "a-good-passphrase",
55 adminIfFirst: true,
56 });
57 if (!made.ok) throw new Error(made.error);
58 alice = made.value;
59 const repo = await createRepo(app, alice, { name: "lib", visibility: "public" });
60 if (!repo.ok) throw new Error(String(repo.error));
61 lib = repo.value;
62 const seeded = await commitToBranch(app, alice, lib, {
63 message: "Start",
64 files: [{ path: "README.md", content: "# lib\n" }],
65 });
66 if (!seeded.ok) throw new Error(JSON.stringify(seeded.error));
67 app.db.query("UPDATE repo SET require_review = 1 WHERE id = ?1").run(lib.id);
68 lib = { ...lib, requireReview: true };
69 a = await push("a.md");
70 const opened = await openPull(app, alice, lib, {
71 title: "Proposed",
72 baseBranch: "main",
73 headBranch: "child/kid/one",
74 });
75 if (!opened.ok) throw new Error(String(opened.error));
76 recordPullProposal(app.db, opened.value.id, {
77 child: "kid",
78 origin: "https://kid.example.com/",
79 });
80 issueId = opened.value.id;
81 number = opened.value.number;
82 }, 120_000);
83
84 afterAll(async () => {
85 app.close();
86 await rm(dataDir, { recursive: true, force: true });
87 });
88
89 test("a grant on head A lets A run, and a push to B waits for its own grant", async () => {
90 expect(runGrantedAt(app.db, issueId, a)).toBeUndefined();
91 expect(pullMayRun(app.db, lib, pull(), a)).toBe(false);
92
93 grantRun(app.db, issueId, a, alice.id);
94 expect(runGrantedAt(app.db, issueId, a)).toBeString();
95 expect(pullMayRun(app.db, lib, pull(), a)).toBe(true);
96
97 b = await push("b.md");
98 expect(b).not.toBe(a);
99 expect(runGrantedAt(app.db, issueId, b)).toBeUndefined();
100 expect(pullMayRun(app.db, lib, pull(), b)).toBe(false);
101
102 grantRun(app.db, issueId, b, alice.id);
103 expect(pullMayRun(app.db, lib, pull(), b)).toBe(true);
104 });
105
106 test("granting twice keeps one row, moves granted_at and records the second granter", async () => {
107 app.db
108 .query("UPDATE pull_run_grant SET granted_at = '2000-01-01T00:00:00.000Z', user_id = 'first' WHERE issue_id = ?1 AND commit_oid = ?2")
109 .run(issueId, a);
110 const before = rows().length;
111 grantRun(app.db, issueId, a, alice.id);
112 grantRun(app.db, issueId, a, alice.id);
113 expect(rows()).toHaveLength(before);
114 expect(runGrantedAt(app.db, issueId, a)! > "2000-01-01T00:00:00.000Z").toBe(true);
115 const row = app.db
116 .query<{ user_id: string }, [string, string]>(
117 "SELECT user_id FROM pull_run_grant WHERE issue_id = ?1 AND commit_oid = ?2",
118 )
119 .get(issueId, a)!;
120 expect(row.user_id).toBe(alice.id);
121 });
122
123 test("an empty oid is never granted", () => {
124 const before = rows().length;
125 grantRun(app.db, issueId, "", alice.id);
126 expect(runGrantedAt(app.db, issueId, "")).toBeUndefined();
127 expect(rows()).toHaveLength(before);
128 expect(pullMayRun(app.db, lib, pull(), "")).toBe(false);
129 });
130
131 test("a grant is not a review: the merge gate still wants one", async () => {
132 expect(pullMayRun(app.db, lib, pull(), b)).toBe(true);
133 const refused = await mergePull(app, alice, lib, pull(), "merge");
134 expect(refused.ok).toBe(false);
135 if (!refused.ok) expect(refused.error).toBe("review_required");
136 expect(
137 app.db.query("SELECT COUNT(*) AS n FROM review WHERE issue_id = ?1").get(issueId),
138 ).toEqual({ n: 0 });
139 });
140 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs