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

A test pins who sees a sealed issue: `mayView`, `canSeeSealed` and `mayViewId` #6805

Merged opened this

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

Merged

scout/forge:scout/6788-a-test-pins-who-sees-a-sealed-issue-mayv → main 1 commit fork

  • Merged. scout/6788-a-test-pins-who-sees-a-sealed-issue-mayv was merged into main as 41cd73b.

Review

Sign in to review this change.
1 file changed +194 −0
test/sealed-predicates.test.ts added +194
@@ -0,0 +1,194 @@
1 /**
2 * Who sees a sealed issue (#621): the predicates of `core/sealed.ts`, called
3 * directly on a seeded repository. An unsealed issue is for anyone who can
4 * read; a sealed one is for its author while active, the repository's
5 * writers and its crew; a run or event about it answers the same way, and
6 * the preview of a sealed pull says only yes or no.
7 */
8 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
9 import { mkdtemp, rm } from "node:fs/promises";
10 import { tmpdir } from "node:os";
11 import { join } from "node:path";
12 import { createApp, type App } from "../src/app.ts";
13 import { addToCrew, createAgent } from "../src/actions/crew.ts";
14 import { createRepo } from "../src/actions/repos.ts";
15 import { setSuspended } from "../src/core/accounts.ts";
16 import { createIssue, type Issue } from "../src/core/issues.ts";
17 import type { Repo } from "../src/core/repos.ts";
18 import { createRun } from "../src/core/runs.ts";
19 import {
20 canSeeSealed,
21 mayView,
22 mayViewId,
23 sealedPreview,
24 sealedScope,
25 } from "../src/core/sealed.ts";
26 import { createUser, getUserById, type User } from "../src/core/users.ts";
27 import { quietLogs } from "./helpers/globals.ts";
28
29 quietLogs();
30
31 let dataDir: string;
32 let app: App;
33 let owner: User;
34 let author: User;
35 let stranger: User;
36 let crew: User;
37 let outsideAgent: User;
38 let repo: Repo;
39 let open: Issue;
40 let sealed: Issue;
41 let sealedPull: Issue;
42
43 const ok = <T>(
44 r: { ok: true; value: T } | { ok: false; error: unknown },
45 ): T => {
46 if (!r.ok) throw new Error(JSON.stringify(r.error));
47 return r.value;
48 };
49
50 const person = async (username: string, first = false) =>
51 ok(
52 await createUser(app.db, {
53 origin: "seed",
54 trusted: true,
55 username,
56 email: `${username}@forge.local`,
57 password: "a-good-passphrase",
58 adminIfFirst: first,
59 }),
60 );
61
62 beforeAll(async () => {
63 dataDir = await mkdtemp(join(tmpdir(), "forge-sealed-pred-"));
64 app = createApp({ dataDir, port: 0 });
65 owner = await person("alice", true);
66 author = await person("reporter");
67 stranger = await person("stranger");
68 repo = ok(
69 await createRepo(app, owner, { name: "square", visibility: "public" }),
70 );
71 crew = ok(await createAgent(app, owner, { username: "bot" })).user;
72 outsideAgent = ok(await createAgent(app, owner, { username: "drifter" })).user;
73 expect(addToCrew(app, owner, repo, "bot").ok).toBe(true);
74 const file = (title: string, sealedFlag: boolean, kind?: "pull") =>
75 ok(
76 createIssue(app.db, {
77 repoId: repo.id,
78 authorId: author.id,
79 title,
80 body: `body of ${title}`,
81 sealed: sealedFlag,
82 ...(kind ? { kind } : {}),
83 }),
84 );
85 open = file("Open question", false);
86 sealed = file("Secret hole", true);
87 sealedPull = file("Secret fix", true, "pull");
88 });
89
90 afterAll(async () => {
91 app.db.close();
92 await rm(dataDir, { recursive: true, force: true });
93 });
94
95 describe("mayView", () => {
96 test("an unsealed issue is visible to anyone who can read the repository", () => {
97 for (const who of [undefined, stranger, author, owner, crew])
98 expect(mayView(app.db, repo, open, who)).toBe(true);
99 });
100
101 test("a sealed issue is visible to its author, a writer and the crew", () => {
102 expect(mayView(app.db, repo, sealed, author)).toBe(true);
103 expect(mayView(app.db, repo, sealed, owner)).toBe(true);
104 expect(mayView(app.db, repo, sealed, crew)).toBe(true);
105 });
106
107 test("a sealed issue is not visible to a stranger, a reader or no one", () => {
108 expect(mayView(app.db, repo, sealed, stranger)).toBe(false);
109 expect(mayView(app.db, repo, sealed, undefined)).toBe(false);
110 // An agent outside this crew is a reader like any other.
111 expect(mayView(app.db, repo, sealed, outsideAgent)).toBe(false);
112 });
113
114 test("the author is not its own reader once suspended", () => {
115 setSuspended(app.db, author.id, "2026-01-01T00:00:00.000Z");
116 try {
117 const suspended = getUserById(app.db, author.id)!;
118 expect(mayView(app.db, repo, sealed, suspended)).toBe(false);
119 expect(mayView(app.db, repo, open, suspended)).toBe(true);
120 } finally {
121 setSuspended(app.db, author.id, null);
122 }
123 expect(mayView(app.db, repo, sealed, getUserById(app.db, author.id))).toBe(
124 true,
125 );
126 });
127
128 test("the author sees only their own: another's sealed issue stays shut", () => {
129 const theirs = { sealed: true, authorId: stranger.id };
130 expect(mayView(app.db, repo, theirs, author)).toBe(false);
131 });
132 });
133
134 describe("canSeeSealed and sealedScope", () => {
135 test("only writers and the crew see sealed issues of a repository", () => {
136 expect(canSeeSealed(app.db, repo, owner)).toBe(true);
137 expect(canSeeSealed(app.db, repo, crew)).toBe(true);
138 expect(canSeeSealed(app.db, repo, author)).toBe(false);
139 expect(canSeeSealed(app.db, repo, stranger)).toBe(false);
140 expect(canSeeSealed(app.db, repo, outsideAgent)).toBe(false);
141 expect(canSeeSealed(app.db, repo, undefined)).toBe(false);
142 });
143
144 test("a list includes all, one's own, or none", () => {
145 expect(sealedScope(app.db, repo, owner)).toEqual({ sealed: true });
146 expect(sealedScope(app.db, repo, author)).toEqual({ sealedBy: author.id });
147 expect(sealedScope(app.db, repo, undefined)).toEqual({});
148 });
149 });
150
151 describe("mayViewId", () => {
152 test("a run or event about an issue gets the issue's answer", () => {
153 const run = createRun(app.db, {
154 repoId: repo.id,
155 kind: "agent",
156 issueId: sealed.id,
157 });
158 for (const who of [undefined, stranger, outsideAgent, author, owner, crew]) {
159 expect(mayViewId(app.db, repo, run.issueId, who)).toBe(
160 mayView(app.db, repo, sealed, who),
161 );
162 expect(mayViewId(app.db, repo, open.id, who)).toBe(true);
163 }
164 expect(mayViewId(app.db, repo, run.issueId, stranger)).toBe(false);
165 expect(mayViewId(app.db, repo, run.issueId, owner)).toBe(true);
166 });
167
168 test("no issue id is visible to all; an unknown id is no secret", () => {
169 for (const who of [undefined, stranger, author, owner])
170 expect(mayViewId(app.db, repo, null, who)).toBe(true);
171 expect(mayViewId(app.db, repo, "no-such-issue", undefined)).toBe(true);
172 });
173 });
174
175 describe("sealedPreview", () => {
176 test("names only whether a preview is a sealed pull's, never its title or body", () => {
177 const env = `pull-${sealedPull.number}`;
178 const answer = sealedPreview(app.db, repo.id, env);
179 expect(answer).toBe(true);
180 expect(typeof answer).toBe("boolean");
181 expect(JSON.stringify(answer)).not.toContain("Secret");
182 expect(sealedPreview(app.db, repo.id, `pull-${open.number}`)).toBe(false);
183 expect(sealedPreview(app.db, repo.id, "pull-99999")).toBe(false);
184 expect(sealedPreview(app.db, repo.id, "production")).toBe(false);
185 });
186
187 test("a stranger is refused what the preview gate would show", () => {
188 // The gate asks `sealedPreview`, then `mayView` on the row: a stranger fails it.
189 expect(sealedPreview(app.db, repo.id, `pull-${sealedPull.number}`)).toBe(true);
190 expect(mayView(app.db, repo, sealedPull, stranger)).toBe(false);
191 expect(mayView(app.db, repo, sealedPull, undefined)).toBe(false);
192 expect(mayView(app.db, repo, sealedPull, crew)).toBe(true);
193 });
194 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs