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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/201-the-studio-home-badges-an-app-failed-whi → main 1 commit fork
3 files
changed +45
−16
src/web/studio/shelf.ts +4 −15
| @@ -33,7 +33,7 @@ import { repoDir } from "../../git/repos.ts"; | ||
| 33 | 33 | import type { Ctx } from "../../http/context.ts"; |
| 34 | 34 | import { repoBase } from "../chrome.ts"; |
| 35 | 35 | 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"; | |
| 37 | 37 | import { decisionsFor, type Decision } from "./asks.ts"; |
| 38 | 38 | import { withLikely } from "./leans.ts"; |
| 39 | 39 | import { pullWord, runWord, type WordCtx } from "./words.ts"; |
| @@ -104,7 +104,6 @@ export async function shelfRows( | ||
| 104 | 104 | ? [] |
| 105 | 105 | : runtime.status(record); |
| 106 | 106 | const production = envs.find((e) => e.env === "production"); |
| 107 | const state = production?.state ?? "stopped"; | |
| 108 | 107 | const latest = latestRuns.get(repo.id); |
| 109 | 108 | // A run still in flight is the thing that is moving, and outranks a |
| 110 | 109 | // change sitting open; once nothing is moving, an open change — an |
| @@ -132,12 +131,8 @@ export async function shelfRows( | ||
| 132 | 131 | ? undefined |
| 133 | 132 | : actors.get(latest.actorId)?.username; |
| 134 | 133 | 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); | |
| 141 | 136 | const happening = |
| 142 | 137 | word === undefined |
| 143 | 138 | ? stuck.has(repo.id) |
| @@ -164,13 +159,7 @@ export async function shelfRows( | ||
| 164 | 159 | ? html`${repo.description} · ` |
| 165 | 160 | : null}${happening}`, |
| 166 | 161 | 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>`, | |
| 174 | 163 | }); |
| 175 | 164 | })); |
| 176 | 165 | } |
test/studio-words.test.ts +9
| @@ -90,6 +90,15 @@ const DEV_WORDS: readonly { pattern: RegExp; because: string }[] = [ | ||
| 90 | 90 | pattern: /Close issue\b/, |
| 91 | 91 | because: "the studio's own word for a tell is tell", |
| 92 | 92 | }, |
| 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 | }, | |
| 93 | 102 | ]; |
| 94 | 103 | |
| 95 | 104 | /** |
test/studio.test.ts +32 −1
| @@ -206,7 +206,7 @@ describe("the studio", () => { | ||
| 206 | 206 | // The bytes the home rendered before the row moved into its own |
| 207 | 207 | // module, escaped so no formatter can reflow them. |
| 208 | 208 | 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>", | |
| 210 | 210 | ); |
| 211 | 211 | }); |
| 212 | 212 | |
| @@ -2119,6 +2119,21 @@ describe("the studio head says why the app is not up", () => { | ||
| 2119 | 2119 | redirect: "manual", |
| 2120 | 2120 | }).then((r) => r.text()); |
| 2121 | 2121 | |
| 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 | ||
| 2122 | 2137 | const signIn = async (username: string): Promise<string> => |
| 2123 | 2138 | ( |
| 2124 | 2139 | ( |
| @@ -2251,6 +2266,17 @@ describe("the studio head says why the app is not up", () => { | ||
| 2251 | 2266 | expect(theirs).not.toContain('href="/alice/notes/app/production/open"'); |
| 2252 | 2267 | }); |
| 2253 | 2268 | |
| 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 | ||
| 2254 | 2280 | test("a stopped production says it is not running and offers Redeploy, not Stop — and no live link to the host", async () => { |
| 2255 | 2281 | put({ lastRunId: deploy }); |
| 2256 | 2282 | const mine = await head(writer); |
| @@ -2259,6 +2285,7 @@ describe("the studio head says why the app is not up", () => { | ||
| 2259 | 2285 | expect(mine).not.toContain('action="/alice/notes/app/production/stop"'); |
| 2260 | 2286 | expect(mine).not.toContain('href="/alice/notes/app/production/open"'); |
| 2261 | 2287 | expect(mine).toContain('title="Not running"'); |
| 2288 | expect(await shelfBadge(writer)).toBe("not running"); | |
| 2262 | 2289 | }); |
| 2263 | 2290 | |
| 2264 | 2291 | 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", () => { | ||
| 2269 | 2296 | expect(mine).toContain("starting"); |
| 2270 | 2297 | expect(mine).not.toContain('href="/alice/notes/app/production/open"'); |
| 2271 | 2298 | 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"); | |
| 2272 | 2302 | }); |
| 2273 | 2303 | |
| 2274 | 2304 | 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", () => { | ||
| 2287 | 2317 | expect(mine).not.toContain("not up"); |
| 2288 | 2318 | expect(mine).toContain("serving · tests failed: 1 of 3 tests failed"); |
| 2289 | 2319 | expect(mine).toContain('href="/alice/notes/app/production/open"'); |
| 2320 | expect(await shelfBadge(writer)).toBe("serving · tests failed"); | |
| 2290 | 2321 | }); |
| 2291 | 2322 | }); |
| 2292 | 2323 |
new above
offline