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

Home tells the truth in few words: figures agree with their sources, blurbs become lines #10652

Merged opened this in answer to

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

Merged

scout/forge:scout/10440-home-tells-the-truth-in-few-words-figure → main 4 commits fork 6 threads · 6 unresolved

  • Merged. scout/10440-home-tells-the-truth-in-few-words-figure was merged into main as 1012a20.

Review

Sign in to review this change.
19 files changed +153 −56
docs/features/living-home.md +9
@@ -17,3 +17,12 @@ The feed — the composer, the timeline, what is on you, since you were here —
1717 Left out on purpose: the waiting-on-you line, the clog, vitals and keeper lines (the boards and the Flow dialog still show them), and "slowest right now" (it needs a timing read the home does not have). The box posts to `/tell` today; it becomes the `/talk` entry (#7652) when that lands.
1818
1919 The two scans behind the lists are read once per write (`writeStamp`), so a warm home reads no tables for them.
20
21 ## The figures tell the truth (#10440)
22
23 - **Slots** in "Builders running" are `crew.concurrency` (`crewConcurrency` in `src/flow/slot_cap.ts`), or the repositories' slots when those are fewer: the sum of the repositories' slots is a ceiling, not a limit. Home and the terminal read the same figure.
24 - **"Nothing ready"** is said only when the repository's map has nothing ready (`/api/v1/repos/:owner/:name/map`); `test/home-builders-idle.test.ts` checks home against that answer.
25 - **Pace line:** "Behind pace" only when the used percent is below the elapsed percent; otherwise "Under the allowed burn".
26 - **Just shipped** titles are cut at a word (`shortTitle`); the row's hover and screen-reader title carry the whole one.
27 - **Dropped on purpose** (the issue named them: no decision uses them): the Traffic, Reporting browsers and Accounts tiles. The same numbers stay on `/settings/traffic`, `/boards/reach` and the KPI probe.
28 - A stuck approved pull is one line ("… waits for a person to merge it."); the wall it hit is on the pull.
src/crew/idle_reason.ts +5 −2
@@ -19,7 +19,7 @@ import { standings } from "../core/standing.ts";
1919 import { grantedAccounts } from "../core/subscriptions.ts";
2020 import { slottedRuns, spentToday } from "./autonomy.ts";
2121 import { atCeiling } from "./dispatch.ts";
22 import { heldNow } from "./held.ts";
22 import { heldNow, HOLDS } from "./held.ts";
2323 import { pace } from "./pacing.ts";
2424
2525 export interface IdleReason {
@@ -54,7 +54,10 @@ export function idleReason(app: App, now: Date): IdleReason | null {
5454 }
5555
5656 const held = heldNow(app, repo);
57 return held.startable.length === 0 ? { word: "nothing ready", href: "/map" } : null;
57 if (held.startable.length > 0) return null;
58 // "Nothing ready" is true only when the map's ready list is empty; otherwise the ready issues are all held.
59 const ready = HOLDS.reduce((n, h) => n + held.by[h].length, 0);
60 return { word: ready === 0 ? "nothing ready" : "none startable", href: "/map" };
5861 }
5962 return null;
6063 }
src/crew/week.ts +2 −1
@@ -149,6 +149,7 @@ export function weekDecision(input: WeekInput): WeekDecision | null {
149149 ? `under a point over the last ${WEEK.burnHours} h (the meter moves in whole points and the crew ran)`
150150 : `${pct(burn)} an hour over the last ${WEEK.burnHours} h`;
151151 // `full`: the free percent and run-out time are said here; otherwise the crew line above says them.
152 const behindLabel = used < (1 - left / WINDOW_MS.week) * 100 ? "Behind pace" : "Under the allowed burn";
152153 const say = (full: boolean): string => {
153154 const weekLeft = full ? `${Math.round(meter.freePct)}% of the week left with ${timeGone}` : `the week has ${timeGone}`;
154155 const runOut = full && runsOutAt !== null ? `, running out about ${utc(runsOutAt)}` : "";
@@ -158,7 +159,7 @@ export function weekDecision(input: WeekInput): WeekDecision | null {
158159 ? `On pace: ${weekLeft}; burning ${burnWords}, ${pct(allowed)} is allowed.`
159160 : status === "ahead"
160161 ? `Ahead of pace: ${weekLeft}, burning ${burnWords} against ${pct(allowed)} allowed${runOut}; ${slots === 0 ? "holding the crew for the reset" : `slowing to ${slots} of ${ceiling} slots`}.`
outdated

The label is chosen by an inline ternary inside a template string, and it recomputes elapsed time with (1 - left / WINDOW_MS.week) * 100 where say already knows the pace. Compute const label = ... once above and reuse it. The line is now unreadable.

161 : `Behind pace: ${weekLeft}, burning ${burnWords} against ${pct(allowed)} allowed; ${slots === 0 ? "holding the crew for the reset" : `speeding up to ${slots} of ${ceiling} slots`}${release ? " and releasing the next issue" : ""}.`;
162 : `${behindLabel}: ${weekLeft}, burning ${burnWords} against ${pct(allowed)} allowed; ${slots === 0 ? "holding the crew for the reset" : `speeding up to ${slots} of ${ceiling} slots`}${release ? " and releasing the next issue" : ""}.`;
162163 };
163164 return { status, targetPctPerHour, allowedPctPerHour: allowed, runsOutAt, slots, release, sentence: say(true), besideCrewLine: say(false) };
164165 }
src/flow/alarm_stuck.ts +1 −1
@@ -140,7 +140,7 @@ export function alarmStuck(from: App | Database, now: Date, scope: StuckScope =
140140 repo,
141141 number,
142142 wall,
143 says: `${repo}#${number} is approved and waits for a person: ${wall.join("; ")}. Allow it (merge) or send it back.`,
143 says: `${repo}#${number} is approved and waits for a person to merge it.`,
144144 };
145145 return {
146146 fired: true,
src/flow/kpis.ts +3 −1
@@ -19,6 +19,7 @@
1919 import { backupVerdict, isWatched, type BackupVerdict } from "../snapshot/bucket.ts";
2020 import { lastDrill } from "../snapshot/drills.ts";
2121 import type { Database } from "bun:sqlite";
22 import { crewConcurrency, shownSlots } from "./slot_cap.ts";
2223 import { spentTodayEverywhere } from "../crew/autonomy.ts";
2324 import { ceilingHitAt, ceilingReached } from "./ceiling.ts";
2425 import type { IdleReason } from "../crew/idle_reason.ts";
@@ -78,7 +79,8 @@ export function readBuilders(db: Database, idle: IdleReason | null = null): Kpis
7879 (SELECT COALESCE(SUM(autonomy_slots), 0) FROM repo WHERE autonomy != 'off') AS slots`,
7980 )
8081 .get() ?? { running: 0, slots: 0 };
81 return { ...counts, idle };
82 const slots = shownSlots(counts.slots, crewConcurrency(db));
outdated

Hand-rolled IIFE with a try/catch that swallows every error and returns NaN. Catching 'no settings table' this way hides real failures. Read the value through one named helper that returns a number or null, with no exception for control flow. Put the min in buildersValue or a small module, not inline in the reader.

83 return { ...counts, slots, idle };
8284 }
8385
8486 /**
src/flow/moving.ts +16 −1
@@ -15,6 +15,8 @@ import type { PipelineStage } from "./pipeline.ts";
1515 export interface MovingRow {
1616 readonly who: string;
1717 readonly title: string;
18 /** The title whole, for a hover or a screen reader: cutting `title` loses nothing. */
19 readonly full: string;
1820 readonly href: string;
1921 readonly at: string;
2022 /** `owner/name`, for the row's title attribute. */
@@ -22,6 +24,18 @@ export interface MovingRow {
2224 }
2325
2426 export const MOVING_ROWS = 3;
27 /** Longest title a home row shows. */
28 export const TITLE_MAX = 60;
29
30 /** A title cut at a word boundary, with an ellipsis; whole when it fits. */
31 export function shortTitle(title: string, max = TITLE_MAX): string {
32 const t = title.replace(/\s+/g, " ").trim();
33 if (t.length <= max) return t;
34 const cut = t.slice(0, max);
35 const at = cut.lastIndexOf(" ");
outdated

The regex strips a trailing -, –, ., : before the ellipsis, but when the cut is mid-word with no space past max/2 it still chops mid-word. The test only covers the happy path. Add cases for a long unbroken token and for whitespace-only collapse. Also keep full titles reachable (title attribute) so cutting loses nothing for a screen reader or a hover.

36 // One long unbroken token has no boundary to cut at: it is cut at the limit.
37 return `${(at > max / 2 ? cut.slice(0, at) : cut).replace(/[\s:,;.\-–]+$/, "")}…`;
38 }
2539 /** Rows read before the viewer's access filters them; bounded, so the read is. */
2640 const SCAN = 40;
2741
@@ -66,7 +80,8 @@ function visible(db: Database, viewer: User, raw: readonly Raw[], path: string,
6680 if (repo === undefined) continue;
6781 out.push({
6882 who: r.who ?? "",
69 title: r.title,
83 title: shortTitle(r.title),
84 full: r.title.replace(/\s+/g, " ").trim(),
7085 href: `/${repo.ownerName}/${repo.name}/${r.kind === undefined ? path : r.kind === "pull" ? "pulls" : "issues"}/${r.number}`,
7186 at: r.at,
7287 repo: `${repo.ownerName}/${repo.name}`,
src/flow/slot_cap.ts added +19
@@ -0,0 +1,19 @@
1 /**
2 * How many builders the crew may run at once, as home and the terminal say it:
3 * the repositories' slots add up to a ceiling, `crew.concurrency` is the limit.
4 */
5 import type { Database } from "bun:sqlite";
6 import { capacityValue } from "../core/capacity.ts";
7
8 /** The crew's concurrency now, or null when the setting holds no positive whole number. */
9 export function crewConcurrency(db: Database): number | null {
10 // A database that never had settings (a bare fixture) has no limit to read.
11 if (db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'forge_setting'").get() === null) return null;
12 const n = Number(capacityValue(db, "crew.concurrency"));
13 return Number.isInteger(n) && n > 0 ? n : null;
14 }
15
16 /** The slots to show: the repositories' sum, never more than the crew runs at once. */
17 export function shownSlots(sum: number, cap: number | null): number {
18 return cap === null ? sum : Math.min(sum, cap);
19 }
src/web/studio/kpi_row.ts +1 −19
@@ -11,7 +11,7 @@
1111 */
1212
1313 import type { Database } from "bun:sqlite";
14 import { KPI_COUNTS, backupFull, backupOverdue, backupValue, buildersValue, checkTime, reactNote, shippedValue, speedWords, spendCeilingWords, spendFull, spendValue, trafficValue, trafficWords, usersValue, visitorsNote, visitorsValue, waitingWords } from "../../flow/kpi_words.ts";
14 import { KPI_COUNTS, backupFull, backupOverdue, backupValue, buildersValue, checkTime, reactNote, shippedValue, speedWords, spendCeilingWords, spendFull, spendValue, waitingWords } from "../../flow/kpi_words.ts";
1515 import { perfRecord } from "../../perf/live.ts";
1616 import type { TrafficClass, TrafficMinute, TrafficTotals } from "../../perf/traffic.ts";
1717 import type { IdleReason } from "../../crew/idle_reason.ts";
@@ -80,11 +80,6 @@ export function sparkline(hours: readonly number[]): Html {
8080 return spark([...hours].reverse(), "Shipped each hour over the last 24 hours");
8181 }
8282
83 /** Distinct visitors per day, oldest first, today on the right. */
84 export function visitorsSpark(days: readonly number[]): Html {
85 return spark(days, `Distinct visitors each day over the last ${days.length} days`);
86 }
87
8883 /** Lightest first, stacked from the bottom: the strangers sit on top and read darkest. */
8984 export const TRAFFIC_TONES: readonly (readonly [TrafficClass, string])[] = [
9085 ["internal", "0.12"],
@@ -124,7 +119,6 @@ export function kpiRowFor(k: Kpis, live: { sha: string; at: string } | null, now
124119 const overdue = k.backup ? k.backup.state === "red" : backupOverdue(k.backupAt, now);
125120 const spendWords = spendCeilingWords(k.spend);
126121 const rate = shippedValue(k.landed, k.landedHours.length);
127 const trafficSays = traffic === null ? null : trafficWords(traffic);
128122 return html`<section class="stats kpi-row mb" aria-label="The forge's figures, each over its own window; hover one for what it counts">
129123 ${tile(
130124 "Shipped per hour",
@@ -189,18 +183,6 @@ export function kpiRowFor(k: Kpis, live: { sha: string; at: string } | null, now
189183 null,
190184 k.bugs.overdue > 0 ? "attn" : null,
191185 )}
192 ${traffic === null || trafficSays === null ? null : tile(
193 "Traffic, 15 min",
194 "/settings/traffic",
195 trafficValue(traffic),
196 `${traffic.requests} requests, ${traffic.refused} refused`,
197 trafficSays.note,
198 trafficSays.title,
199 trafficColumns(traffic.minutes, W, H, "Answering time each minute over the last 15 minutes, by who asked"),
200 trafficSays.attn ? "attn" : null,
201 )}
202 ${tile("Reporting browsers", "/boards/reach", visitorsValue(k.reach), `${visitorsValue(k.reach)}, ${visitorsNote(k.reach)}`, visitorsNote(k.reach), KPI_COUNTS.visitors, visitorsSpark(k.reach.visitorDays))}

Dead code: visitorsSpark has no caller once the Reporting browsers tile is gone. Remove it, and any now-unused import.

203 ${tile("Accounts", "/boards/reach", usersValue(k.reach), usersValue(k.reach), "seen today", KPI_COUNTS.accounts)}
204186 ${tile(
205187 "Reported page speed",
206188 "/boards/reach",
src/web/studio/living.ts +1 −1
@@ -59,7 +59,7 @@ function listOf(label: string, rows: readonly MovingRow[], empty: string, headin
5959 row({
6060 href: r.href,
6161 ...(r.who === "" ? {} : { lead: html`<span class="muted small">${r.who}</span>` }),
62 title: html`<span class="truncate" title="${r.repo}">${r.title}</span>`,
62 title: html`<span class="truncate" title="${r.repo}: ${r.full}">${r.title}</span>`,
6363 trail: html`<span class="muted small">${timeAgo(r.at)}</span>`,
6464 }),
6565 );
test/flow-alarm-stuck.test.ts +2 −1
@@ -99,7 +99,8 @@ describe("alarmStuck", () => {
9999 wall("h");
100100 const a = alarmStuck(app, NOW);
101101 expect(a.wall).toEqual(["anyone signed out could read pack records"]);
102 expect(a.says).toContain("waits for a person: anyone signed out could read pack records");
102 expect(a.says).toBe(`${a.repo}#${a.number} is approved and waits for a person to merge it.`);
103 expect(a.says.length).toBeLessThan(80);
103104 });
104105
105106 test("an event 29 minutes ago does not fire", () => {
test/home-builders-idle.test.ts +43
@@ -92,7 +92,15 @@ async function terminal(): Promise<string> {
9292 /** The builders tile's anchor on home. */
9393 const tile = (page: string): string => /<a class="stat" href="[^"]*"[^>]*><span class="stat-label">Builders running.*?<\/a>/s.exec(page)?.[0] ?? "";
9494
95 /** The repository's map as the API answers it: what home's "nothing ready" must agree with. */
96 async function mapReady(): Promise<number> {
97 const res = await fetch(`${servers.url}/api/v1/repos/alice/engine/map`, { headers: { cookie: `forge_session=${createSession(app.db, admin.id).token}` } });
98 expect(res.status).toBe(200);
99 return ((await res.json()) as { ready: unknown[] }).ready.length;
100 }
101
95102 test("nothing ready: the map has nothing a builder may start", async () => {
103 expect(await mapReady()).toBe(0);
96104 const page = await home();
97105 expect(tile(page)).toContain("nothing ready");
98106 expect(tile(page)).toContain('href="/map"');
@@ -102,6 +110,7 @@ test("nothing ready: the map has nothing a builder may start", async () => {
102110 test("a ready issue and a builder with room: no reason is named", async () => {
103111 const opened = openIssue(app, admin, repo, { title: "Renderer" });
104112 if (!opened.ok) throw new Error(String(opened.error));
113 expect(await mapReady()).toBeGreaterThan(0);
105114 const page = await home();
106115 expect(tile(page)).not.toContain("nothing ready");
107116 expect(tile(page)).toContain("running now");
@@ -191,3 +200,37 @@ test("a cached reason is dropped when the counts change: one of two slots runnin
191200 reason = null;
192201 expect(cachedKpis(app.db, now, forge).builders).toEqual({ running: 1, slots: 2, idle: null });
193202 });
203
204 test("slots shown are the crew's concurrency, not the sum of the repositories' ceilings (#10440)", async () => {
205 const wide = await updateRepoSettings(app, admin, repo, { autonomy: "autonomous", autonomyDirection: "Build.", autonomyCeilingUsd: 10, autonomySlots: 16, autonomyBy: admin.id });
206 expect(wide.ok).toBe(true);
outdated

Raw SQL built by interpolating the id into a string. Use bound parameters, like the repo's other tests. The test also leaves 'crew.concurrency' at 40 on failure, so use try/finally or afterEach cleanup so a failing assertion does not poison later tests.

207 const set = (v: string) =>
208 app.db.query("INSERT OR REPLACE INTO forge_setting (key, value, updated_by, updated_at) VALUES ('crew.concurrency', ?1, ?2, datetime('now'))").run(v, admin.id);
209 try {
210 set("4");
211 const page = await home();
212 expect(tile(page)).toMatch(/>\d of 4</);
213 expect(tile(page)).not.toContain("of 16");
214 expect(kpiLines(readKpis(app.db, new Date()), null).find((l) => l.startsWith("Builders running"))).toMatch(/Builders running \d of 4/);
215 // Never more than the repositories offer.
216 set("40");
217 expect(readKpis(app.db, new Date()).builders.slots).toBe(16);
218 } finally {
219 app.db.query("DELETE FROM forge_setting WHERE key = 'crew.concurrency'").run();
220 }
221 });
222
223 test("the slots figure is crew.concurrency on home and in the terminal, and the running figure is the runs' own count (#10440)", async () => {
224 const wide = await updateRepoSettings(app, admin, repo, { autonomy: "autonomous", autonomyDirection: "Build.", autonomyCeilingUsd: 10, autonomySlots: 16, autonomyBy: admin.id });
225 expect(wide.ok).toBe(true);
226 try {
227 app.db.query("INSERT OR REPLACE INTO forge_setting (key, value, updated_by, updated_at) VALUES ('crew.concurrency', '3', ?1, datetime('now'))").run(admin.id);
228 const running = app.db.query<{ n: number }, []>("SELECT COUNT(*) AS n FROM run WHERE kind = 'agent' AND state = 'running'").get()!.n;
229 const text = await terminal();
230 expect(text).toContain(`Builders running ${running} of 3`);
231 expect(text).not.toContain("of 16");
232 expect(tile(await home())).toContain(`${running} of 3`);
233 } finally {
234 app.db.query("DELETE FROM forge_setting WHERE key = 'crew.concurrency'").run();
235 }
236 });
test/home-kpi-page.test.ts +3 −7
@@ -25,7 +25,7 @@ afterAll(async () => {
2525 await rm(dataDir, { recursive: true, force: true });
2626 });
2727
28 test("home renders the eleven tiles; only an admin sees them", async () => {
28 test("home renders the eight tiles; only an admin sees them", async () => {
2929 const mk = async (username: string, first = false): Promise<User> => {
3030 const r = await createUser(app.db, {
3131 origin: "seed",
@@ -60,7 +60,7 @@ test("home renders the eleven tiles; only an admin sees them", async () => {
6060 view(yesterday, "z", 400, 160);
6161 const a = await home(admin);
6262 expect(a).toContain('class="stats kpi-row mb"');
63 expect((a.match(/class="stat"/g) ?? []).length).toBe(11);
63 expect((a.match(/class="stat"/g) ?? []).length).toBe(8);
6464 for (const href of ["/board", "/releases", "/boards", "/settings/crew", "/settings/backups"]) expect(a).toContain(`class="stat" href="${href}"`);
6565 // The tiles are §5 stats (#3982): none of the page-local kpi classes is left on home.
6666 for (const name of ["kpis", "kpi", "kpi-value", "kpi-note", "kpi-spark"]) expect(a).not.toMatch(new RegExp(`class="[^"]*\\b${name}\\b(?!-)`));
@@ -68,16 +68,12 @@ test("home renders the eleven tiles; only an admin sees them", async () => {
6868
6969 // 2 accounts, the admin alone with a session today; visitors 3 today and 4 over 7 days;
7070 // load p75 of [100,200,300,400,1900] is 400 ms, INP p75 of [40,80,120,160] is 120 ms.
71 expect(a).toContain(">1 of 2<");
72 expect(a).toContain(">3 today<");
73 expect(a).toContain(">4 in 7 days<");
7471 expect(a).toContain(">400 ms<");
7572 expect(a).toContain(">reacts in 120 ms<");
76 expect((a.match(/class="stat" href="\/boards\/reach"/g) ?? []).length).toBe(3);
73 expect((a.match(/class="stat" href="\/boards\/reach"/g) ?? []).length).toBe(1);
7774
7875 // The figures count every repository, so a non-admin sees none.
7976 expect(a).toContain("no forge ceiling");
80 expect(a).toContain("last 30 days");
8177 expect(await home(bob)).not.toContain('class="stats kpi-row mb"');
8278 expect(await home(bob)).not.toContain("last 30 days");
8379 expect(await home()).not.toContain('class="stats kpi-row mb"');
test/home-kpi-row.test.ts +4 −6
@@ -256,8 +256,10 @@ describe("the reach tiles", () => {
256256 test("say what they count in words, each linking to the reach board", () => {
257257 const reach = { usersToday: 3, usersTotal: 12, visitorsToday: 5, visitors7d: 31, visitorDays: new Array<number>(30).fill(0), loadP75: 1250, inpP75: 180 };
258258 const out = kpiRowFor({ landedHours: hours(0), landed: 0, live: null, backupAt: null, reach, medianCheckMs: null, builders: { running: 0, slots: 0, idle: null }, spend: { todayUsd: 0, ceilingUsd: null } }, null).toString();
259 for (const v of ["5 today", "31 in 7 days", "3 of 12", "seen today", "1.3 s", "reacts in 180 ms"]) expect(out).toContain(`>${v}<`);
260 expect((out.match(/class="stat" href="\/boards\/reach"/g) ?? []).length).toBe(3);
259 for (const v of ["1.3 s", "reacts in 180 ms"]) expect(out).toContain(`>${v}<`);
260 // Reporting browsers and Accounts feed no decision: only the page speed stays (#10440).
261 for (const gone of ["Reporting browsers", "Accounts", "Traffic"]) expect(out).not.toContain(gone);
262 expect((out.match(/class="stat" href="\/boards\/reach"/g) ?? []).length).toBe(1);
261263 expect(out).not.toContain("INP");
262264 expect(out).not.toContain("p75");
263265 });
@@ -289,10 +291,6 @@ describe("the visitors sparkline", () => {
289291 expect(r.visitorDays[29]).toBe(2);
290292 expect(r.visitorDays[0]).toBe(1); // 2026-09-20 is 29 days back; 09-19 is out
291293 expect(r.visitorDays.reduce((a, b) => a + b, 0)).toBe(3);

The sparkline test that asserted 30 points and the reach link was deleted instead of moved. The reach computation (visitorDays) is still tested, but nothing renders it now. That is more evidence the spark is dead; remove it together with the test.

292 const out = kpiRowFor({ landedHours: hours(0), landed: 0, live: null, backupAt: null, reach: r, medianCheckMs: null, builders: { running: 0, slots: 0, idle: null }, spend: { todayUsd: 0, ceilingUsd: null } }, null).toString();
293 const svg = /<svg[^>]*last 30 days[^>]*><polyline points="([^"]+)"/.exec(out)!;
294 expect(svg[1]!.split(" ")).toHaveLength(30);
295 expect(out).toContain('href="/boards/reach"');
296294 });
297295 });
298296
test/home-kpis.test.ts +2 −2
@@ -145,8 +145,8 @@ describe("home kpi row titles", () => {
145145 });
146146
147147 test("no value or note lacks a title, and none brings back a git word", () => {
148 expect(full.match(/class="stat-(value num|note)"/g)).toHaveLength(18);
149 expect(full.match(/class="stat-(value num|note)" title=/g)).toHaveLength(18);
148 expect(full.match(/class="stat-(value num|note)"/g)).toHaveLength(14);
149 expect(full.match(/class="stat-(value num|note)" title=/g)).toHaveLength(14);
150150 expect(full).not.toMatch(/commit|merge|branch|repositor|pull request/i);
151151 });
152152 });
test/idle-reason.test.ts +3 −2
@@ -129,11 +129,12 @@ describe("idleReason", () => {
129129 expect(idleReason(app, new Date())?.word).toBe("ceiling reached");
130130 });
131131
132 test("an issue the dispatcher holds is not ready: a run in flight on the only one says nothing ready", () => {
132 test("an issue the dispatcher holds is not ready: a run in flight on the only one says none startable, not nothing ready", () => {
133133 const only = open("Held by a run");
134134 const run = createRun(app.db, { repoId: repo.id, kind: "agent", actorId: bot.id, reason: "review", issueId: only.id });
135135 set("UPDATE run SET state = 'running' WHERE id = ?1", run.id);
136 expect(idleReason(app, NOW)).toEqual({ word: "nothing ready", href: "/map" });
136 // The map still lists it as ready: "nothing ready" would be false.
137 expect(idleReason(app, NOW)).toEqual({ word: "none startable", href: "/map" });
137138 });
138139
139140 test("the line says the word and the link", () => {
test/kpi-spark.test.ts +1 −7
@@ -6,7 +6,7 @@
66 */
77
88 import { describe, expect, test } from "bun:test";
9 import { daySpark, sparkline, visitorsSpark } from "../src/web/studio/kpi_row.ts";
9 import { daySpark, sparkline } from "../src/web/studio/kpi_row.ts";
1010
1111 const LINE = 'fill="none" stroke="currentColor" stroke-width="1.5"';
1212
@@ -49,12 +49,6 @@ describe("daySpark", () => {
4949 });
5050
5151 describe("the numbers-only sparklines are unchanged", () => {
52 test("visitorsSpark", () => {
53 expect(visitorsSpark([0, 2, 4]).toString()).toBe(
54 `<svg viewBox="0 0 96 24" width="96" height="24" role="img" aria-label="Distinct visitors each day over the last 3 days"><polyline points="0.0,22.0 48.0,12.0 96.0,2.0" ${LINE}/></svg>`,
55 );
56 });
57
5852 test("sparkline, newest first in, oldest on the left", () => {
5953 expect(sparkline([2, 0]).toString()).toBe(
6054 `<svg viewBox="0 0 96 24" width="96" height="24" role="img" aria-label="Shipped each hour over the last 24 hours"><polyline points="0.0,22.0 96.0,2.0" ${LINE}/></svg>`,
test/moving-title.test.ts added +19
@@ -0,0 +1,19 @@
1 import { expect, test } from "bun:test";
2 import { shortTitle, TITLE_MAX } from "../src/flow/moving.ts";
3
4 test("a title that fits is whole; a long one is cut at a word, never mid-word", () => {
5 expect(shortTitle("Fix the build")).toBe("Fix the build");
6 const long = "Sentinel: release was degraded 180 minutes today: Release 0.7.495 is unproven: https://example.test did not answer";
7 const cut = shortTitle(long);
8 expect(cut.length).toBeLessThanOrEqual(TITLE_MAX + 1);
9 expect(cut.endsWith("…")).toBe(true);
10 expect(long.startsWith(cut.slice(0, -1))).toBe(true);
11 expect(long[cut.length - 1]).toBe(" ");
12 });
13
14 test("one long unbroken token is cut at the limit, and whitespace collapses first", () => {
15 const token = "x".repeat(200);
16 expect(shortTitle(token)).toBe(`${"x".repeat(TITLE_MAX)}…`);
17 expect(shortTitle("a b \n c")).toBe("a b c");
18 expect(shortTitle(`word ${token}`)).toBe(`word ${"x".repeat(TITLE_MAX - 5)}…`);
19 });
test/traffic-truth.test.ts +9 −5
@@ -206,16 +206,15 @@ test("the tile says plainly when strangers hold most of the time, and not for a
206206 expect(trafficWords(t).note).toContain("too much");
207207 expect(trafficWords(t).title).toStartWith("Something is wrong");
208208 const row = String(kpiRowFor(emptyKpis(), null, NOW, t));
209 expect(row).toMatch(
210 /<a class="stat" href="\/settings\/traffic" data-tone="attn"/,
211 );
209 // The home row no longer carries a Traffic tile; the board at /settings/traffic does.
210 expect(row).not.toContain("/settings/traffic");
212211
213212 const handful = createPerfRecord();
214213 handful.add("/a", sample(5000, { deepStranger: true }));
215214 expect(trafficAttention(handful.ledger.totals(END))).toBeNull();
216215 expect(
217216 String(kpiRowFor(emptyKpis(), null, NOW, handful.ledger.totals(END))),
218 ).not.toMatch(/traffic" data-tone="attn"/);
217 ).not.toContain("/settings/traffic");
219218
220219 expect(trafficValue(createPerfRecord().ledger.totals(END))).toBe("—");
221220 });
@@ -290,13 +289,18 @@ test("every tile's value equals the probe's, from known requests and page_vital
290289 for (const n of probe.numbers) {
291290 expect(n.counts.length).toBeGreaterThan(10);
292291 expect([n.id, n.agrees, n.recomputed]).toEqual([n.id, true, n.value]);
292 // These figures are the probe's and the boards', no longer tiles on the home row.
293 if (["traffic", "visitors", "users", "accounts"].includes(n.id)) {
294 expect([n.id, tiles.has(n.label)]).toEqual([n.id, false]);
295 continue;
296 }
293297 const shown = tiles.get(n.label);
294298 expect([n.label, n.id === "live" ? shown !== undefined : shown]).toEqual([
295299 n.label,
296300 n.id === "live" ? true : n.value,
297301 ]);
298302 }
299 expect(tiles.size).toBe(probe.numbers.length);
303 expect(tiles.size).toBe(probe.numbers.length - 3);
300304 // The live tile's short form is the start of the probe's full id.
301305 expect(byId.get("live")!.value.startsWith(tiles.get("Live version")!)).toBe(
302306 true,
test/week.test.ts +10
@@ -171,3 +171,13 @@ describe("efficiency", () => {
171171 expect(efficiencyWords({ landings: 0, runs: 2, costUsd: 4 })).toContain("Nothing has landed");
172172 });
173173 });
174
175 describe("the pace words (#10440)", () => {
176 test("`Behind pace` only when less is used than the time gone; otherwise it says what is true", () => {
177 // base: 84 h of 168 left, so half the week is gone.
178 expect(used(20, { burnPctPerHour: 0 }).sentence).toContain("Behind pace");
179 const heavy = used(70, { burnPctPerHour: 0 });
180 expect(heavy.sentence).not.toContain("Behind pace");
181 expect(heavy.sentence).toContain("Under the allowed burn");
182 });
183 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs