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

/board/returns, what the window's dollars bought, speaks the home page's language #9899

Merged opened this

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

Merged

scout/forge:scout/9896-board-returns-what-the-window-s-dollars → main 3 commits fork 1 thread · 1 unresolved

  • Merged. scout/9896-board-returns-what-the-window-s-dollars was merged into main as b2f7d6c.

Review

Sign in to review this change.
2 files changed +122 −19
src/web/pages/returns.ts +32 −19
@@ -14,7 +14,7 @@ import type { Ctx } from "../../http/context.ts";
1414 import { notFoundPage, signInFirst } from "../chrome.ts";
1515 import { html, type Html } from "../html.ts";
1616 import { page } from "../layout.ts";
17 import { card, listCard, row } from "../ui.ts";
17 import { row } from "../ui.ts";
1818 import { usd } from "../../core/money.ts";
1919
2020 const runs = (m: Money): string => `${m.runs} ${m.runs === 1 ? "run" : "runs"}`;
@@ -26,6 +26,16 @@ const line = (title: string, m: Money, note?: string): Html =>
2626 trail: html`<b>${usd(m.usd)}</b>`,
2727 });
2828
29 /** A plain section under its own h2, rows in a list; no card (#9896). */
30 const section = (id: string, heading: string, rows: Html[], note?: string): Html =>
31 html`<section class="stack" aria-labelledby="${id}">
32 <h2 id="${id}">${heading}</h2>
33 ${note === undefined ? null : html`<p class="muted small">${note}</p>`}
34 <ul class="list">
35 ${rows}
36 </ul>
37 </section>`;
38
2939 /** GET /board/returns */
3040 export function returnsPage(ctx: Ctx): Response {
3141 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
@@ -80,7 +90,9 @@ export function returnsPage(ctx: Ctx): Response {
8090 ${usd(nothing.closed.usd)} closed with nothing · ${usd(nothing.open.usd)} still open
8191 </p>
8292 <div class="stack">
83 ${listCard(
93 ${section(
94 "returns-where",
95 "Where it went",
8496 [
8597 line(
8698 "On work that landed",
@@ -90,20 +102,22 @@ export function returnsPage(ctx: Ctx): Response {
90102 line("On work closed without a merge", nothing.closed),
91103 line("On work still open, nothing merged yet", nothing.open),
92104 ],
93 { head: html`<h2>Where it went</h2>` },
94105 )}
95 ${listCard(
106 ${section(
107 "returns-not-landed",
108 "What did not land, and why",
96109 [
97110 line("Runs that failed", nothing.failed),
98111 // Reasons are a part of "Runs that failed", not beside it: said so.
99112 ...nothing.topFailures.map((f) => line(`of which: ${f.reason}`, f)),
100113 line("Runs cancelled", nothing.cancelled),
101114 ],
102 { head: html`<h2>What did not land, and why</h2>` },
103115 )}
104116 ${r.byPillar.length === 0
105117 ? null
106 : listCard(
118 : section(
119 "returns-by-pillar",
120 "By pillar",
107121 r.byPillar.map((b) =>
108122 row({
109123 title: b.pillar,
@@ -115,9 +129,10 @@ export function returnsPage(ctx: Ctx): Response {
115129 trail: html`<b>${usd(b.usd)}</b>`,
116130 }),
117131 ),
118 { head: html`<h2 id="returns-by-pillar">By pillar</h2>` },
119132 )}
120 ${listCard(
133 ${section(
134 "returns-costliest",
135 "What cost the most",
121136 r.costliest.map((c) => {
122137 const repo = repos.get(c.repoId);
123138 const slug = repo ? `${repo.ownerName}/${repo.name}` : "";
@@ -133,11 +148,12 @@ export function returnsPage(ctx: Ctx): Response {
133148 trail: html`<b>${usd(c.usd)}</b>`,
134149 });
135150 }),
136 { head: html`<h2>What cost the most</h2>` },
137151 )}
138152 ${r.outside.length === 0
139153 ? null
140 : listCard(
154 : section(
155 "returns-outside",
156 "Reported from outside",
141157 r.outside.map((o) =>
142158 row({
143159 title: o.who,
@@ -146,19 +162,16 @@ export function returnsPage(ctx: Ctx): Response {
146162 trail: html`<b>${usd(o.usd)}</b>`,
147163 }),
148164 ),
149 {
150 head: html`<h2>Reported from outside</h2>
151 <span class="muted small">what sessions the forge did not run say they spent; not in the figures above</span>`,
152 },
165 "what sessions the forge did not run say they spent; not in the figures above",
153166 )}
154 ${card(
155 html`<p>
167 <section class="stack" aria-labelledby="returns-run">
168 <h2 id="returns-run">A run</h2>
169 <p>
156170 A run that finished well costs ${usd(r.okRun.usd.median)} and takes
157171 ${r.okRun.minutes.median} minutes (median); one in ten costs over
158172 ${usd(r.okRun.usd.p90)} or runs over ${r.okRun.minutes.p90} minutes.
159 </p>`,
160 { head: html`<h2>A run</h2>` },
161 )}
173 </p>
174 </section>
162175 </div>`;
163176 const response = page({ ctx, title: "What the tokens bought", body });
164177 notePhase(ctx.request, "render", performance.now() - rendering);
test/returns-language.test.ts added +90
@@ -0,0 +1,90 @@
1 /** /board/returns speaks the home page's language (#9896): one h1, each part a plain section under an h2, no card. */
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 { openIssue } from "../src/actions/issues.ts";
8 import { createRepo } from "../src/actions/repos.ts";
9 import { addDependency } from "../src/core/dependencies.ts";
10 import { recordOutsideSpend } from "../src/core/outside_spend.ts";
11 import { createRun } from "../src/core/runs.ts";
12 import { createSession } from "../src/core/sessions.ts";
13 import { createUser, getUserByName } from "../src/core/users.ts";
14 import { serve, type Servers } from "../src/http/server.ts";
15
16 let dir = "";
17 let app: App;
18 let servers: Servers;
19 let cookie = "";
20
21 beforeAll(async () => {
22 dir = await mkdtemp(join(tmpdir(), "forge-returns-lang-"));
23 app = createApp({ dataDir: dir, port: 0 });
24 servers = await serve(app);
25 const u = await createUser(app.db, {
26 origin: "seed", trusted: true, username: "alice", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true,
27 });
28 if (!u.ok) throw new Error("user");
29 cookie = `forge_session=${createSession(app.db, u.value.id).token}`;
30 });
31 afterAll(async () => {
32 servers.stop();
33 app.close();
34 await rm(dir, { recursive: true, force: true });
35 });
36
37 const main = (page: string): string => page.slice(page.indexOf("<main"), page.indexOf("</main>"));
38
39 test("the owner's /board/returns: one h1, plain sections under h2s in order, no card", async () => {
40 const res = await fetch(`${servers.url}/board/returns`, { headers: { cookie } });
41 expect(res.status).toBe(200);
outdated

Principle-neutral but required: the expected headings omit 'By pillar' and 'Reported from outside'. Seed data for both and assert the full list in order, so every converted section is covered by a test that fails on main.

42 const page = main(await res.text());
43 expect(page.match(/<h1[\s>]/g)?.length).toBe(1);
44 expect(page).not.toContain('class="card');
45 expect(page).not.toContain("card-body");
46 const heads = [...page.matchAll(/<section class="stack" aria-labelledby="[^"]+">\s*<h2 [^>]*>([^<]+)<\/h2>/g)].map((m) => m[1]);
47 expect(heads).toEqual(["Where it went", "What did not land, and why", "What cost the most", "A run"]);
48 });
49
50 test("?days= still widens the window and a stranger is still turned away", async () => {
51 const res = await fetch(`${servers.url}/board/returns?days=30`, { headers: { cookie } });
52 expect(await res.text()).toContain("30 days");
53 const anon = await fetch(`${servers.url}/board/returns`, { redirect: "manual" });
54 expect(anon.status).not.toBe(200);
55 });
56
57 test("with a pillar and an outside report: every part is a plain section, in order, A run last", async () => {
58 const alice = getUserByName(app.db, "alice")!;
59 const made = await createRepo(app, alice, { name: "mill", visibility: "public" });
60 if (!made.ok) throw new Error(String(made.error));
61 const repo = made.value;
62 const work = openIssue(app, alice, repo, { title: "Some work" });
63 const umbrella = openIssue(app, alice, repo, { title: "P1 Ship: it" });
64 if (!work.ok || !umbrella.ok) throw new Error("issues");
65 addDependency(app.db, umbrella.value, work.value, alice.id);
66 const r = createRun(app.db, { repoId: repo.id, kind: "agent", issueId: work.value.id, actorId: alice.id, reason: "assigned" });
67 app.db
68 .query("UPDATE run SET state = 'failed', cost_usd = 2.5, summary = 'failed', started_at = ?2, finished_at = ?3 WHERE id = ?1")
69 .run(r.id, new Date(Date.now() - 600_000).toISOString(), new Date().toISOString());
70 recordOutsideSpend(app.db, {
71 repoId: repo.id, issueId: null, reporterId: alice.id, who: "the build team",
72 model: "", tokensIn: 1000, tokensOut: 200, usd: 3.25, note: "",
73 });
74 const res = await fetch(`${servers.url}/board/returns`, { headers: { cookie } });
75 expect(res.status).toBe(200);
76 const page = main(await res.text());
77 expect(page.match(/<h1[\s>]/g)?.length).toBe(1);
78 expect(page).not.toContain('class="card');
79 expect(page).not.toContain("card-body");
80 const heads = [...page.matchAll(/<section class="stack" aria-labelledby="[^"]+">\s*<h2 [^>]*>([^<]+)<\/h2>/g)].map((m) => m[1]);
81 expect(heads).toEqual([
82 "Where it went",
83 "What did not land, and why",
84 "By pillar",
85 "What cost the most",
86 "Reported from outside",
87 "A run",
88 ]);
89 expect(page).toContain("the build team");
90 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs