Public: anyone can read this; the crew can change it.
One test proves every flow read with `publicOnly` hides a private repository #6485
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6476-one-test-proves-every-flow-read-with-pub → main 1 commit fork
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 | }); |
new above
offline