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

Calm: a test fails when the owner's home or board renders a banner, alert or red notice for a setting #11385

Merged opened this

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

Merged

scout/forge:scout/11379-calm-a-test-fails-when-the-owner-s-home → main 2 commits fork 3 threads · 3 unresolved

  • Merged. scout/11379-calm-a-test-fails-when-the-owner-s-home was merged into main as 0b5b1e0.

Review

Sign in to review this change.
1 file changed +156 −0
test/calm-home.test.ts added +156
@@ -0,0 +1,156 @@
1 /**
2 * Calm (#11379): the admin's home and board, with the forge deliberately odd
3 * (egress in report mode, a repository on autonomy with no reviewer on its
4 * crew, the week's spend at its lowest), carry no role="alert", no .banner and
5 * no .notice-danger element above the main content. The odd facts are one line
6 * each: egress and the week's share in Settings › Capacity, the missing
7 * reviewer as the board's one `data-need` line.
8 *
9 * Low credit: the forge has no credit-balance state (`core/credit.ts` is
10 * authorship credit; the only spend knob is `week.use`), so the lowest week
11 * share stands for it. When a balance exists, add it here.
12 *
13 * EXCEPTIONS is the whole list of things allowed to stand on these pages by
14 * name, each the real markup its source renders. Neither trips the detector
15 * today; they are named so a future change that makes one loud has to say so.
16 * Keep it short: a setting is never on it.
17 */
18 import { afterAll, beforeAll, expect, test } from "bun:test";
19 import { readFileSync } from "node:fs";
20 import { mkdtemp as mkTmp, rm as rmDir } from "node:fs/promises";
outdated

Neither selector exists in src/. Use the real markup of a failed deploy and a stalled flow card, and render each in a test.

21 import { tmpdir } from "node:os";
22 import { join } from "node:path";
23 import { addToCrew, createAgent } from "../src/actions/crew.ts";
24 import { openPull } from "../src/actions/pulls.ts";
25 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
26 import { createApp, type App } from "../src/app.ts";
27 import { setCapacity } from "../src/core/capacity.ts";
28 import { claimRun, createRun, finishRun } from "../src/core/runs.ts";
29 import { createApp as makeAppRecord } from "../src/core/apps.ts";
30 import { createSession } from "../src/core/sessions.ts";
31 import { serve, type Servers } from "../src/http/server.ts";
outdated

Drop export; unused outside this file.

32 import { EGRESS_REPORTED } from "../src/system/egress_line.ts";
33 import { seedAdmin } from "./helpers/admin.ts";
34
35 /**
36 * Named exceptions, each the markup its source renders:
37 * - a stalled flow card with its one action: `data-flow-card` (src/web/studio/flow_card.ts);
38 * - a failed deploy: the shelf row's `badge` with `data-tone` attn/danger (src/web/studio/shelf.ts).
39 */
40 const EXCEPTIONS: Record<string, RegExp> = {
41 "stalled flow card": /\bdata-flow-card=/,
42 "failed deploy": /\bclass="badge"[^>]*\bdata-tone="(?:attn|danger)"/,
43 };
44
45 const TAG = /<[a-z][^<>]*?(?:\brole="alert"|\bclass="[^"]*\b(?:banner|notice-danger)\b[^"]*")[^<>]*>/gi;
46 const found = (body: string): string[] =>
47 [...body.matchAll(TAG)].map((m) => m[0]).filter((t) => !Object.values(EXCEPTIONS).some((re) => re.test(t)));
48 /** Alarms anywhere above the first <main>, and in a <header>/<nav>, wherever it sits. */
49 const region = (body: string): string => {
outdated

Also set up the no-reviewer state (and low credit if a state exists) explicitly.

50 const at = body.search(/<main[\s>]/i);
51 const above = at < 0 ? body : body.slice(0, at);
52 const chrome = [...body.matchAll(/<(header|nav)[\s>][\s\S]*?<\/\1>/gi)].map((m) => m[0]).join("\n");
53 return `${above}\n${chrome}`;
54 };
55 const alarms = (body: string): string[] => [...new Set(found(region(body)))];
56
57 let dataDir = "", cookie = "", repoId = "";
58 let app: App, servers: Servers;
59 let n = 0;
60 const hoursAgo = (h: number) => new Date(Date.now() - h * 3_600_000).toISOString();
61
62 beforeAll(async () => {
63 dataDir = await mkTmp(join(tmpdir(), "forge-calmhome-"));
64 app = createApp({ dataDir, port: 0 });
65 servers = await serve(app);
66 const admin = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" });
67 if (!admin.ok) throw new Error(admin.error);
68 const alice = admin.value;
69 cookie = `forge_session=${createSession(app.db, alice.id).token}`;
70 setCapacity(app.db, "cells.egress", "report", alice.id);
71 setCapacity(app.db, "week.use", "10", alice.id);
72 // No reviewer: an autonomous repository whose crew holds a builder and nobody else.
73 const made = await createRepo(app, alice, { name: "garden", visibility: "public" });
74 if (!made.ok) throw new Error("repo");
75 repoId = made.value.id;
76 await commitToBranch(app, alice, made.value, { message: "seed", files: [{ path: "README.md", content: "# garden\n" }] });
77 await createAgent(app, alice, { username: "diggerbot" });
78 if (!addToCrew(app, alice, made.value, "diggerbot", "builder").ok) throw new Error("crew");
79 app.db.query("UPDATE repo SET autonomy = 'autonomous' WHERE id = ?1").run(repoId);
80 makeAppRecord(app.db, { repoId, slug: "garden", access: "private" }); // the board lists apps
81 // A stalled flow step: a pull whose check went red hours ago.
82 const branch = `b${++n}`;
83 const c = await commitToBranch(app, alice, made.value, { branch, base: "main", message: branch, files: [{ path: `${branch}.md`, content: "x\n" }] });
84 if (!c.ok) throw new Error("branch");
85 const pull = await openPull(app, alice, made.value, { title: "Stuck change", body: "", baseBranch: "main", headBranch: branch });
86 if (!pull.ok) throw new Error("pull");
87 app.db.query("UPDATE issue SET created_at = ?2 WHERE id = ?1").run(pull.value.id, hoursAgo(5));
88 const r = createRun(app.db, { repoId, kind: "check", issueId: pull.value.id, env: `pull-${pull.value.number}`, commitOid: pull.value.headOid, reason: "pull" });
89 claimRun(app.db, r.id);
90 finishRun(app.db, r.id, { state: "failed", summary: "red" });
91 app.db.query("UPDATE run SET created_at = ?2, finished_at = ?2 WHERE id = ?1").run(r.id, hoursAgo(4));
92 }, 30_000);
93
94 afterAll(async () => {
95 servers.stop();
96 app.close();
97 await rmDir(dataDir, { recursive: true, force: true });
98 });
99
100 const get = async (path: string): Promise<string> => {
101 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
102 expect(res.status).toBe(200);
103 return res.text();
104 };
105
106 test("home and board paint no alert, banner or danger notice for the odd settings", async () => {
107 for (const path of ["/", "/board"]) {
108 const body = await get(path);
109 expect(body.length).toBeGreaterThan(500);
110 expect({ path, found: alarms(body) }).toEqual({ path, found: [] });
111 expect(body).not.toContain(EGRESS_REPORTED.slice(0, 30));
112 }
113 });
114
115 test("no reviewer is the board's one line, a link and not an alert", async () => {
116 const body = await get("/board");
117 expect(body.split("No reviewer: agent pulls wait for you").length - 1).toBe(1);
118 expect(body).toMatch(/<a data-need href="\/asks"[^>]*>[^<]*<[^>]*>[^<]*<\/span>?/s);
119 });
120
121 test("a stalled flow card renders on home with its one action, and is spared", async () => {
122 // The home's feed fragment, as the page loads it in studio mode.
123 const res = await fetch(`${servers.url}/feed`, { headers: { cookie: `${cookie}; forge_mode=studio` } });
124 const body = await res.text();
125 expect(alarms(body)).toEqual([]);
126 const card = body.match(/<[^>]*\bdata-flow-card="[^"]*"[^>]*>/)?.[0] ?? "";
127 expect(card).not.toBe("");
128 expect(body).toContain("Re-run");
129 expect(EXCEPTIONS["stalled flow card"]!.test(card)).toBe(true);
130 expect(found(`${card}<div role="alert">x</div>`)).toHaveLength(1);
131 expect(found(`<div role="alert" ${card.slice(4, -1)}>x</div>`)).toEqual([]);
132 });
133
134 test("a failed deploy's badge is the markup shelf.ts renders, and is spared", () => {
135 const source = readFileSync(join(import.meta.dir, "../src/web/studio/shelf.ts"), "utf8");
136 expect(source).toContain('<span class="badge" data-tone="${tone}">');
137 expect(source).toContain('? "attn"');
138 expect(found('<span class="badge" data-tone="attn">failed</span>')).toEqual([]);
139 expect(found('<span class="badge" data-tone="attn" role="alert">failed</span>')).toEqual([]);
140 });
141
142 test("the odd facts show in Settings as one line each", async () => {
143 const body = await get("/settings/capacity");
144 expect(body.split('id="egress-reported"').length - 1).toBe(1);
145 expect(body).toContain(EGRESS_REPORTED);
146 expect(body).toContain("Share of the week to use");
147 });
148
149 test("the region scan catches a banner in a page's header or nav, even after <main> opens", () => {
150 expect(alarms('<header><div class="banner">x</div></header><main>y</main>')).toHaveLength(1);
151 expect(alarms('<main>y</main><nav><p role="alert">x</p></nav>')).toHaveLength(1);
152 expect(alarms('<div class="stack banner">x</div>')).toHaveLength(1);
153 expect(alarms('<p class="notice-danger">x</p>')).toHaveLength(1);
154 expect(alarms('<main><div role="alert" data-x>content</div></main>')).toEqual([]);
155 expect(Object.keys(EXCEPTIONS)).toHaveLength(2);
156 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs