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

One test proves every flow read with `publicOnly` hides a private repository #6485

Merged opened this

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

Merged

scout/forge:scout/6476-one-test-proves-every-flow-read-with-pub → main 1 commit fork

  • Merged. scout/6476-one-test-proves-every-flow-read-with-pub was merged into main as 96ff4b1.

Review

Sign in to review this change.
1 file changed +137 −0
test/flow-public-only.test.ts added +137
@@ -0,0 +1,137 @@
1 /** One test proves every flow read that takes `publicOnly` hides a private repository (#6476). */
2 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { createUser } from "../src/core/users.ts";
8 import { alarmStuck } from "../src/flow/alarm_stuck.ts";
9 import { readChecksPile } from "../src/flow/checks_pile.ts";
10 import { conflictedPulls, noteWaitingToFinish } from "../src/flow/conflicted.ts";
11 import { readOldestApproved } from "../src/flow/oldest_approved.ts";
12 import { readRedBatches } from "../src/flow/red_batches.ts";
13 import { requestedWaits } from "../src/flow/requested.ts";
14 import { reviewersBusy } from "../src/flow/reviewing.ts";
15 import { readVitals } from "../src/flow/vitals.ts";
16
17 const NOW = new Date("2026-10-26T12:00:00.000Z");
18 const ago = (m: number): string => new Date(NOW.getTime() - m * 60_000).toISOString();
19
20 let dir: string;
21 let app: App;
22 let owner: string;
23 let n = 0;
24
25 /** Every state the reads count, in one repository: approved and quiet with a red check, a conflict run, a queued check, changes requested, a review in flight, a landed pull, a red batch. */
26 function seed(repo: string, name: string, reviewer: string): void {
27 const db = app.db;
28 const base = ++n * 10;
29 const pull = (k: number, merged = false): string => {
30 const id = `${repo}-p${k}`;
31 db.run(
32 "INSERT INTO issue (id, repo_id, number, kind, title, author_id, state, created_at) VALUES (?1, ?2, ?3, 'pull', 't', ?4, ?5, ?6)",
33 [id, repo, base + k, owner, merged ? "closed" : "open", ago(600)],
34 );
35 db.run("INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, merged_at) VALUES (?1, 'main', ?2, '', 'h', ?3)", [id, `b${base + k}`, merged ? ago(5) : null]);
36 return id;
37 };
38 const run = (id: string, issue: string, kind: string, state: string, extra: { reason?: string; actor?: string } = {}): void => {
39 db.run(
40 "INSERT INTO run (id, repo_id, kind, issue_id, state, commit_oid, reason, actor_id, created_at, finished_at) VALUES (?1, ?2, ?3, ?4, ?5, 'h', ?6, ?7, ?8, ?8)",
41 [id, repo, kind, issue, state, extra.reason ?? "", extra.actor ?? null, ago(repo === "priv" ? 350 : 300)],
42 );
43 };
44 // Approved, quiet, red on its head, and conflicted.
45 const a = pull(1);
46 db.run("INSERT INTO review (id, issue_id, author_id, state, commit_oid, created_at) VALUES (?1, ?2, ?3, 'approved', 'h', ?4)", [`${repo}-rv1`, a, owner, ago(500)]);
47 run(`${repo}-r1`, a, "check", "failed");
48 run(`${repo}-r2`, a, "agent", "failed", { reason: "conflict" });
49 // A check queued for a slot.
50 run(`${repo}-r3`, pull(2), "check", "queued");
51 // Changes requested by the crew's reviewer, who has a review in flight.
52 const c = pull(3);
53 db.run("INSERT INTO repo_crew (repo_id, user_id, role) VALUES (?1, ?2, 'reviewer')", [repo, reviewer]);
54 db.run("INSERT INTO review (id, issue_id, author_id, state, commit_oid, created_at) VALUES (?1, ?2, ?3, 'changes_requested', 'h', ?4)", [`${repo}-rv3`, c, reviewer, ago(200)]);
55 run(`${repo}-r4`, c, "agent", "running", { reason: "review", actor: reviewer });
56 // Landed, and a red batch.
57 pull(4, true);
58 db.run("INSERT INTO queue_batch (repo_id, red, finished_at) VALUES (?1, 1, ?2)", [repo, ago(10)]);
59 // The finishing pass's waiting pull, which has no run.
60 noteWaitingToFinish(db, repo, [{ repo: `alice/${name}`, number: base + 9, since: ago(100), public: repo === "pub" }]);
61 }
62
63 beforeAll(async () => {
64 dir = await mkdtemp(join(tmpdir(), "flow-public-only-"));
65 app = createApp({ dataDir: dir, port: 0 });
66 const mk = async (username: string, first = false): Promise<string> => {
67 const u = await createUser(app.db, { origin: "seed", trusted: true, username, email: `${username}@forge.local`, password: "a-good-passphrase", adminIfFirst: first });
68 if (!u.ok) throw new Error(u.error);
69 return u.value.id;
70 };
71 owner = await mk("alice", true);
72 const pubReviewer = await mk("pubrev");
73 const privReviewer = await mk("privrev");
74 app.db.run("UPDATE user SET is_agent = 1 WHERE id IN (?1, ?2)", [pubReviewer, privReviewer]);
75 app.db.run("INSERT INTO repo (id, owner_id, name, visibility, autonomy) VALUES ('pub', ?1, 'open', 'public', 'auto')", [owner]);
76 app.db.run("INSERT INTO repo (id, owner_id, name, visibility, autonomy) VALUES ('priv', ?1, 'secret', 'private', 'auto')", [owner]);
77 seed("pub", "open", pubReviewer);
78 seed("priv", "secret", privReviewer);
79 });
80 afterAll(async () => {
81 app.close?.();
82 await rm(dir, { recursive: true, force: true });
83 });
84
85 describe("publicOnly hides a private repository from every flow read", () => {
86 test("the seed is visible to a reader who may see it all", () => {
87 expect(alarmStuck(app.db, NOW).says).toContain("alice/secret#");
88 expect(readChecksPile(app.db)).toEqual({ red: 2, queued: 2 });
89 expect(conflictedPulls(app.db).filter((p) => p.repo === "alice/secret")).toHaveLength(2);
90 expect(readOldestApproved(app.db, NOW)).not.toBeNull();
91 expect(readRedBatches(app.db, NOW)).toEqual({ finished: 2, red: 2, rate: 1 });
92 const w = requestedWaits(app.db);
93 expect([...w.builder, ...w.reviewer].filter((p) => p.repo === "alice/secret")).toHaveLength(1);
94 expect(reviewersBusy(app.db).map((r) => r.reviewer).sort()).toEqual(["privrev", "pubrev"]);
95 expect(readVitals(app.db, NOW).open).toBe(6);
96 });
97
98 test("with publicOnly, nothing from the private repository appears", () => {
99 // The private pull has waited longest, so an unfiltered read names it; the public one must still be named.
100 const stuck = alarmStuck(app.db, NOW, { publicOnly: true });
101 expect(stuck.fired).toBe(true);
102 expect(stuck.says).toContain("alice/open#");
103 expect(stuck.says).not.toContain("secret");
104
105 expect(readChecksPile(app.db, true)).toEqual({ red: 1, queued: 1 });
106
107 const conflicted = conflictedPulls(app.db, true);
108 expect(conflicted).toHaveLength(2);
109 expect(conflicted.every((p) => p.repo === "alice/open")).toBe(true);
110
111 expect(readOldestApproved(app.db, NOW, true)?.repo).toBe("open");
112
113 expect(readRedBatches(app.db, NOW, true)).toEqual({ finished: 1, red: 1, rate: 1 });
114
115 const w = requestedWaits(app.db, true);
116 const waiting = [...w.builder, ...w.reviewer];
117 expect(waiting).toHaveLength(1);
118 expect(waiting.every((p) => p.repo === "alice/open")).toBe(true);
119
120 expect(reviewersBusy(app.db, true)).toEqual([{ reviewer: "pubrev", count: 1 }]);
121
122 expect(readVitals(app.db, NOW, true)).toMatchObject({ open: 3, conflicted: 2 });
123 });
124
125 test("a private-only forge reads empty with publicOnly", () => {
126 app.db.run("DELETE FROM repo WHERE id = 'pub'");
127 noteWaitingToFinish(app.db, "pub", []);
128 expect(alarmStuck(app.db, NOW, { publicOnly: true }).fired).toBe(false);
129 expect(readChecksPile(app.db, true)).toEqual({ red: 0, queued: 0 });
130 expect(conflictedPulls(app.db, true)).toEqual([]);
131 expect(readOldestApproved(app.db, NOW, true)).toBeNull();
132 expect(readRedBatches(app.db, NOW, true).finished).toBe(0);
133 expect(requestedWaits(app.db, true)).toEqual({ builder: [], reviewer: [] });
134 expect(reviewersBusy(app.db, true)).toEqual([]);
135 expect(readVitals(app.db, NOW, true)).toMatchObject({ open: 0, opened6h: 0, landed6h: 0, conflicted: 0 });
136 });
137 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs