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

A test pins the held card: a private repository's row of why issues wait is drawn for its owner only #6888

Merged opened this

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

Merged

scout/forge:scout/6857-a-test-pins-the-held-card-a-private-repo → main 1 commit fork

  • Merged. scout/6857-a-test-pins-the-held-card-a-private-repo was merged into main as 68da13c.

Review

Sign in to review this change.
1 file changed +86 −0
test/boards-held.test.ts added +86
@@ -0,0 +1,86 @@
1 /** The held card (#6857): a private repository's row of why issues wait is drawn for its owner only, with the counts `flow/held.ts` holds. */
2 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
3 import { mkdtempSync } from "node:fs";
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 { readHeld } from "../src/flow/held.ts";
9 import { flowPanel } from "../src/web/pages/flow-panel.ts";
10 import { heldCard } from "../src/web/pages/boards-held.ts";
11
12 const NOW = Date.parse("2026-10-27T12:00:00Z");
13 const MARKUP = "<img src=x onerror=alert(1)>";
14 const SECRET = "secret-plans";
15 let app: App;
16 let alice = "";
17
18 function issue(id: string, repo: string, n: number): void {
19 app.db.run("INSERT INTO issue (id, repo_id, number, kind, title, body, author_id, state) VALUES (?1, ?2, ?3, 'issue', 't', '', ?4, 'open')", [id, repo, n, alice]);
20 }
21 function running(repo: string, issueId: string): void {
22 app.db.run("INSERT INTO run (id, repo_id, kind, issue_id, state) VALUES (?1, ?2, 'agent', ?3, 'running')", [`run-${issueId}`, repo, issueId]);
23 }
24
25 beforeAll(async () => {
26 app = createApp({ dataDir: mkdtempSync(join(tmpdir(), "boards-held-")), port: 0 });
27 const u = await createUser(app.db, { origin: "seed", trusted: true, username: "alice", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true });
28 if (!u.ok) throw new Error(u.error);
29 alice = u.value.id;
30 const repo = (id: string, name: string, visibility: string): void => {
31 app.db.run("INSERT INTO repo (id, owner_id, name, visibility, autonomy, autonomy_by) VALUES (?1, ?4, ?2, ?3, 'autonomous', ?4)", [id, name, visibility, alice]);
32 };
33 repo("pub", MARKUP, "public");
34 repo("priv", SECRET, "private");
35 // pub: two startable, one with a run on it. priv: one startable, two with a run.
36 issue("p1", "pub", 1);
37 issue("p2", "pub", 2);
38 issue("p3", "pub", 3);
39 running("pub", "p3");
40 issue("q1", "priv", 1);
41 issue("q2", "priv", 2);
42 issue("q3", "priv", 3);
43 running("priv", "q2");
44 running("priv", "q3");
45 });
46 afterAll(() => app.close?.());
47
48 const row = (page: string, repo: string): string => page.split("<tr data-held-repo=").find((r) => r.startsWith(`"${repo}"`)) ?? "";
49
50 describe("held card", () => {
51 test("the counts are the ones flow/held.ts holds for the seeded holds", () => {
52 const rows = readHeld(app, true, NOW);
53 const pub = rows.find((r) => r.repo === `alice/${MARKUP}`)!;
54 const priv = rows.find((r) => r.repo === `alice/${SECRET}`)!;
55 expect([pub.startable, pub.counts.running]).toEqual([2, 1]);
56 expect([priv.startable, priv.counts.running]).toEqual([1, 2]);
57 const page = String(heldCard(rows));
58 const privRow = row(page, `alice/${SECRET}`);
59 expect(privRow).toContain('data-held="startable"><span class="mono">startable=1<');
60 expect(privRow).toContain('data-held="running"><span class="mono">running=2<');
61 expect(privRow).toContain("1 of 3 ready can start");
62 expect(privRow).not.toContain('data-held="stalled"');
63 });
64
65 test("the owner's panel has both rows", () => {
66 const page = String(flowPanel(app, { kind: "owner" }, new URL("http://x/boards"), "/boards"));
67 expect(page).toContain(`data-held-repo="alice/${SECRET}"`);
68 expect(page).toContain('data-held-repo="alice/&lt;img');
69 });
70
71 test("a reader who cannot read the private repository gets the public row only, and never its name", () => {
72 const cut = readHeld(app, false, NOW);
73 expect(cut.map((r) => r.public)).toEqual([true]);
74 for (const page of [String(heldCard(cut)), String(flowPanel(app, { kind: "agent" }, new URL("http://x/boards"), "/boards"))]) {
75 expect(page).toContain('data-held-repo="alice/&lt;img');
76 expect(page).not.toContain(SECRET);
77 }
78 });
79
80 test("a repository name with markup is escaped", () => {
81 const page = String(heldCard(readHeld(app, true, NOW)));
82 expect(page).not.toContain(MARKUP);
83 expect(page).not.toContain("<img");
84 expect(page).toContain("&lt;img src=x onerror=alert(1)&gt;");
85 });
86 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs