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

The studio home badges an app "failed", which is not one of its words #213

Merged opened this

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

Merged

scout/forge:scout/201-the-studio-home-badges-an-app-failed-whi → main 1 commit fork

  • Merged. scout/201-the-studio-home-badges-an-app-failed-whi was merged into main as be16b14.

Review

Sign in to review this change.
3 files changed +45 −16
src/web/studio/shelf.ts +4 −15
@@ -33,7 +33,7 @@ import { repoDir } from "../../git/repos.ts";
3333 import type { Ctx } from "../../http/context.ts";
3434 import { repoBase } from "../chrome.ts";
3535 import { html, type Html } from "../html.ts";
36 import { dot, pill, row, time, type Tone } from "../ui.ts";
36 import { dot, envPhrase, envTone, pill, row, time } from "../ui.ts";
3737 import { decisionsFor, type Decision } from "./asks.ts";
3838 import { withLikely } from "./leans.ts";
3939 import { pullWord, runWord, type WordCtx } from "./words.ts";
@@ -104,7 +104,6 @@ export async function shelfRows(
104104 ? []
105105 : runtime.status(record);
106106 const production = envs.find((e) => e.env === "production");
107 const state = production?.state ?? "stopped";
108107 const latest = latestRuns.get(repo.id);
109108 // A run still in flight is the thing that is moving, and outranks a
110109 // change sitting open; once nothing is moving, an open change — an
@@ -132,12 +131,8 @@ export async function shelfRows(
132131 ? undefined
133132 : actors.get(latest.actorId)?.username;
134133 const asks = asksByRepo.get(repo.id) ?? 0;
135 const tone: Tone =
136 state === "running"
137 ? "success"
138 : state === "failed"
139 ? "danger"
140 : "default";
134 const tone = envTone(production?.state);
135 const live = envPhrase(production);
141136 const happening =
142137 word === undefined
143138 ? stuck.has(repo.id)
@@ -164,13 +159,7 @@ export async function shelfRows(
164159 ? html`${repo.description} · `
165160 : null}${happening}`,
166161 trail: html`${asks > 0 ? pill(`${asks} asks you`, "accent") : null}
167 <span class="badge" data-tone="${tone}"
168 >${state === "running"
169 ? "live"
170 : state === "stopped"
171 ? "not running"
172 : state}</span
173 >`,
162 <span class="badge" data-tone="${tone}">${live.word}</span>`,
174163 });
175164 }));
176165 }
test/studio-words.test.ts +9
@@ -90,6 +90,15 @@ const DEV_WORDS: readonly { pattern: RegExp; because: string }[] = [
9090 pattern: /Close issue\b/,
9191 because: "the studio's own word for a tell is tell",
9292 },
93 {
94 // Scoped to a badge's own content — "failed" also rides inside the
95 // legitimate `serving · tests failed` phrase (`envPhrase`), which does
96 // not start a badge with the bare word. `stopped` is not here: it is
97 // also a studio word in its own right, for a run the crew stopped on.
98 pattern: /<span class="badge"[^>]*>\s*(?:failed|deploying)\s*<\/span>/,
99 because:
100 "envPhrase's word is the studio's word for production — 'not up' or 'starting', never the runtime's own EnvState (#201)",
101 },
93102 ];
94103
95104 /**
test/studio.test.ts +32 −1
@@ -206,7 +206,7 @@ describe("the studio", () => {
206206 // The bytes the home rendered before the row moved into its own
207207 // module, escaped so no formatter can reflow them.
208208 expect(shelfRow).toBe(
209 "<li>\n <a href=\"/alice/notes\" class=\"row\"\n \n data-nav tabindex=\"-1\"><span class=\"row-lead\"><span\n class=\"dot\"\n \n ></span></span>\n <span class=\"row-main\">\n <span\n class=\"row-title\"\n title=\"a place for notes\"\n >notes</span\n >\n <span class=\"row-meta\">a place for notes · <span class=\"muted\">quiet — tell the crew something</span></span>\n </span>\n <span class=\"row-trail\">\n <span class=\"badge\" data-tone=\"default\"\n >not running</span\n ></span></a>\n </li>",
209 "<li>\n <a href=\"/alice/notes\" class=\"row\"\n \n data-nav tabindex=\"-1\"><span class=\"row-lead\"><span\n class=\"dot\"\n \n ></span></span>\n <span class=\"row-main\">\n <span\n class=\"row-title\"\n title=\"a place for notes\"\n >notes</span\n >\n <span class=\"row-meta\">a place for notes · <span class=\"muted\">quiet — tell the crew something</span></span>\n </span>\n <span class=\"row-trail\">\n <span class=\"badge\" data-tone=\"default\">not running</span></span></a>\n </li>",
210210 );
211211 });
212212
@@ -2119,6 +2119,21 @@ describe("the studio head says why the app is not up", () => {
21192119 redirect: "manual",
21202120 }).then((r) => r.text());
21212121
2122 /** The home's own shelf row for this app — the badge issue #201 is about. */
2123 const shelfBadge = async (cookie: string): Promise<string> => {
2124 const home = await (
2125 await fetch(`${base}/`, {
2126 headers: { cookie: `${cookie}; forge_mode=studio` },
2127 redirect: "manual",
2128 })
2129 ).text();
2130 const at = home.indexOf('href="/alice/notes"');
2131 expect(at).toBeGreaterThan(-1);
2132 const start = home.lastIndexOf("<li>", at);
2133 const row = home.slice(start, home.indexOf("</li>", at) + 5);
2134 return row.match(/<span class="badge"[^>]*>([\s\S]*?)<\/span>/)?.[1] ?? "";
2135 };
2136
21222137 const signIn = async (username: string): Promise<string> =>
21232138 (
21242139 (
@@ -2251,6 +2266,17 @@ describe("the studio head says why the app is not up", () => {
22512266 expect(theirs).not.toContain('href="/alice/notes/app/production/open"');
22522267 });
22532268
2269 // Issue #201: the shelf used to print the runtime's raw `EnvState` for a
2270 // failed or deploying production, disagreeing with the app's own head
2271 // above — which has always spoken the studio's five-ish words.
2272 test("a failed production reads 'not up' on the home shelf too, never the runtime's own word 'failed'", async () => {
2273 put({
2274 lastError: "the deploy failed: <install> exited 1",
2275 lastRunId: deploy,
2276 });
2277 expect(await shelfBadge(writer)).toBe("not up");
2278 });
2279
22542280 test("a stopped production says it is not running and offers Redeploy, not Stop — and no live link to the host", async () => {
22552281 put({ lastRunId: deploy });
22562282 const mine = await head(writer);
@@ -2259,6 +2285,7 @@ describe("the studio head says why the app is not up", () => {
22592285 expect(mine).not.toContain('action="/alice/notes/app/production/stop"');
22602286 expect(mine).not.toContain('href="/alice/notes/app/production/open"');
22612287 expect(mine).toContain('title="Not running"');
2288 expect(await shelfBadge(writer)).toBe("not running");
22622289 });
22632290
22642291 test("a deploying production says so where the button was, not a link to a host nothing is serving yet", async () => {
@@ -2269,6 +2296,9 @@ describe("the studio head says why the app is not up", () => {
22692296 expect(mine).toContain("starting");
22702297 expect(mine).not.toContain('href="/alice/notes/app/production/open"');
22712298 expect(mine).toContain('title="Still deploying"');
2299 // The runtime's own word for this state is "deploying" — the shelf
2300 // says "starting" instead, same as the head above.
2301 expect(await shelfBadge(writer)).toBe("starting");
22722302 });
22732303
22742304 test("a serving app with red tests still reads as serving, with the failure in its own words, and is still open-able", async () => {
@@ -2287,6 +2317,7 @@ describe("the studio head says why the app is not up", () => {
22872317 expect(mine).not.toContain("not up");
22882318 expect(mine).toContain("serving · tests failed: 1 of 3 tests failed");
22892319 expect(mine).toContain('href="/alice/notes/app/production/open"');
2320 expect(await shelfBadge(writer)).toBe("serving · tests failed");
22902321 });
22912322 });
22922323

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs