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

Why ready issues wait, per autonomous repository, speaks the home page's language #9966

Merged opened this

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

Merged

scout/forge:scout/9955-why-ready-issues-wait-per-autonomous-rep → main 3 commits fork 1 thread · 1 unresolved

  • Merged. scout/9955-why-ready-issues-wait-per-autonomous-rep was merged into main as 4a4f3b0.

Review

Sign in to review this change.
2 files changed +75 −6
src/web/pages/boards-held.ts +5 −6
@@ -8,7 +8,7 @@
88 import { HOLDS } from "../../crew/held.ts";
99 import type { HeldRow } from "../../flow/held.ts";
1010 import { html, type Html } from "../html.ts";
11 import { card, meter } from "../ui.ts";
11 import { meter } from "../ui.ts";
1212
1313 function heldCount(kind: string, n: number): Html {
1414 return html`<span data-held="${kind}"><span class="mono">${kind}=${String(n)}</span></span>`;
@@ -27,8 +27,8 @@ function startableMeter(row: HeldRow): Html {
2727
2828 /** Why ready issues cannot start, per autonomous repository (#2244). */
2929 export function heldCard(rows: readonly HeldRow[]): Html {
30 return card(
31 html`<h2>Ready, and why it waits</h2>
30 return html`<section class="settings-block" id="held">
31 <h2>Ready, and why it waits</h2>
3232 <p class="muted small">
3333 Each ready issue a builder could start, or the first filter that holds
3434 it: a run on it, an open pull, the planner, stalled, its run cap,
@@ -71,7 +71,6 @@ export function heldCard(rows: readonly HeldRow[]): Html {
7171 )}
7272 </tbody>
7373 </table>
74 </div>`}`,
75 { id: "held" },
76 );
74 </div>`}
75 </section>`;
7776 }
test/boards-held-language.test.ts added +70
@@ -0,0 +1,70 @@
1 /** The held-ready table speaks the home page's language: a section under an h2, no card (#9955). */
2 import { afterAll, beforeAll, 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, getUserByName } from "../src/core/users.ts";
8 import { createSession } from "../src/core/sessions.ts";
9 import { flowPanel } from "../src/web/pages/flow-panel.ts";
10 import { serve, type Servers } from "../src/http/server.ts";
11
12 let dataDir = "";
13 let app: App, servers: Servers;
14 let owner = "";
15
16 beforeAll(async () => {
17 dataDir = await mkdtemp(join(tmpdir(), "forge-held-lang-"));
18 app = createApp({ dataDir, port: 0 });
19 servers = await serve(app);
20 for (const [username, extra] of [["alice", { adminIfFirst: true }], ["bobby", {}]] as const) {
21 const u = await createUser(app.db, { origin: "seed", trusted: true, username, email: `${username}@forge.local`, password: "correct-horse-battery", ...extra });
22 if (!u.ok) throw new Error(u.error);
23 }
24 const alice = getUserByName(app.db, "alice")!.id;
25 const repo = (id: string, name: string, vis: string): void => {
26 app.db.run("INSERT INTO repo (id, owner_id, name, visibility, autonomy, autonomy_by) VALUES (?1, ?4, ?2, ?3, 'autonomous', ?4)", [id, name, vis, alice]);
27 };
28 repo("pub", "open-one", "public");
29 repo("priv", "secret-plans", "private");
30 for (const [id, r, n] of [["p1", "pub", 1], ["p2", "pub", 2], ["q1", "priv", 1]] as const) {
31 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, r, n, alice]);
32 }
33 app.db.run("INSERT INTO run (id, repo_id, kind, issue_id, state) VALUES ('run-p2', 'pub', 'agent', 'p2', 'running')");
34 owner = `forge_session=${createSession(app.db, alice).token}`;
35 });
36 afterAll(async () => {
37 servers.stop();
38 app.close();
39 await rm(dataDir, { recursive: true, force: true });
40 });
41
42 const held = async (path: string, cookie: string): Promise<string> => {
43 const page = await (await fetch(`${servers.url}${path}`, { headers: { cookie } })).text();
44 const m = page.slice(page.indexOf("<main"), page.indexOf("</main>"));

Box 3 names /boards and /flow; this covers /boards only. Add a /flow case (or a justified assertion that it has no held card) so the issue box is met. Drop the bare block around it.

45 const i = m.indexOf('id="held"');
46 return i < 0 ? "" : m.slice(m.lastIndexOf("<", i), m.indexOf("</section>", i) + 10);
47 };
48
49 test("/boards: held-ready is a section with an h2, no card, one row per repository", async () => {
50 const h = await held("/boards", owner);
51 expect(h).toMatch(/^<section class="settings-block" id="held">\s*<h2>Ready, and why it waits<\/h2>/);
52 expect(h).not.toContain('class="card');
53 expect((h.match(/data-held-repo=/g) ?? []).length).toBe(2);
54 expect(h).toContain('data-held-repo="alice/open-one"');
55 expect(h).toContain("startable=1");
56 expect(h).toContain("running=1");
57 });
58
59 test("/flow draws no held table, so there is no card#held there either", async () => {
60 const page = await (await fetch(`${servers.url}/flow`, { headers: { cookie: owner } })).text();
61 expect(page).toContain("<main");
62 expect(page).not.toContain('id="held"');
63 expect(page).not.toContain("data-held-repo=");
64 });
65
66 test("a reader who cannot read the private repository gets the public row only", () => {
67 const h = String(flowPanel(app, { kind: "agent" }, new URL("http://x/boards"), "/boards"));
68 expect(h).toContain('data-held-repo="alice/open-one"');
69 expect(h).not.toContain("secret-plans");
70 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs