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

A one-minute-old app's front door leads with a week of zeros and a scary statistic #313

Merged opened this

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

Merged

scout/forge:scout/274-a-one-minute-old-app-s-front-door-leads → main 1 commit fork

  • Merged. scout/274-a-one-minute-old-app-s-front-door-leads was merged into main as 348160c.

Review

Sign in to review this change.
6 files changed +370 −55
src/apps/runtime.ts +1 −8
@@ -32,6 +32,7 @@ import {
3232 failAbandonedRuns,
3333 finishRun,
3434 markServing,
35 STOPPED_SUMMARY,
3536 type Run,
3637 } from "../core/runs.ts";
3738 import { Gate } from "../git/env.ts";
@@ -163,14 +164,6 @@ interface Live {
163164 */
164165 const PUSH_REASONS: readonly string[] = ["push", "opened", "merge"];
165166
166 /**
167 * The one summary every cancellation the runtime itself causes by stopping
168 * carries — a run still queued when `stop()` runs, and one aborted mid-step
169 * alike, so `runWord` (`web/studio/words.ts`) has one fact to recognise
170 * rather than two spellings of the same reason.
171 */
172 export const STOPPED_SUMMARY = "the forge stopped";
173
174167 /** How long between two instants, in seconds to one decimal place: `3.4s`. */
175168 function elapsedSeconds(fromIso: string, toIso: string): string {
176169 const ms = Math.max(0, Date.parse(toIso) - Date.parse(fromIso));
src/core/runs.ts +18 −6
@@ -12,6 +12,15 @@ import type { Database } from "bun:sqlite";
1212 import { id } from "../kernel/id.ts";
1313
1414 export type RunKind = "deploy" | "agent" | "check";
15
16 /**
17 * The one summary a run cancelled by the forge's own restart carries — never
18 * the commit's fault, so callers that count failures (`previewLatency`) or
19 * word one (`runWord` in `web/studio/words.ts`) can tell it apart from a
20 * person's own Cancel or a real failure by this one string.
21 */
22 export const STOPPED_SUMMARY = "the forge stopped";
23
1524 /** The kinds that pass judgement on a commit: a preview's deploy, or a repository's own check. */
1625 export const CHECK_KINDS: readonly RunKind[] = ["deploy", "check"];
1726 export const RUN_KINDS: readonly RunKind[] = ["deploy", "agent", "check"];
@@ -695,9 +704,11 @@ const median = (xs: readonly number[]): number => {
695704 * the total but not the split. Beside the median, the same walk counts
696705 * deploys that finished — `ok`, `failed` or `cancelled` — without ever
697706 * serving; one still `queued` or `running` is neither, and counts as
698 * neither. One query, bounded by `limit` and served by the `run_repo`
699 * index (it walks newest-first and stops at the cap), never a scan of
700 * the table.
707 * neither. A deploy the forge's own restart cancelled (`STOPPED_SUMMARY`)
708 * is left out entirely, from both the failure count and the total it is
709 * "of" — the forge's own interruption, never the commit's. One query,
710 * bounded by `limit` and served by the `run_repo` index (it walks
711 * newest-first and stops at the cap), never a scan of the table.
701712 */
702713 export function previewLatency(
703714 db: Database,
@@ -713,7 +724,7 @@ export function previewLatency(
713724 bootSecs: number | null;
714725 finished: 0 | 1;
715726 },
716 [string, string, number]
727 [string, string, string, number]
717728 >(
718729 `SELECT CASE WHEN serving_at IS NOT NULL
719730 THEN (julianday(serving_at) - julianday(created_at)) * 86400 END AS secs,
@@ -725,9 +736,10 @@ export function previewLatency(
725736 FROM run
726737 WHERE repo_id = ?1 AND kind = 'deploy'
727738 AND created_at >= ?2
728 ORDER BY id DESC LIMIT ?3`,
739 AND NOT (state = 'cancelled' AND summary = ?3)
740 ORDER BY id DESC LIMIT ?4`,
729741 )
730 .all(repoId, since, limit);
742 .all(repoId, since, STOPPED_SUMMARY, limit);
731743 const served = rows.filter((r) => r.secs !== null);
732744 const failedN = rows.filter((r) => r.secs === null && r.finished === 1).length;
733745 if (served.length === 0 && failedN === 0) return undefined;
src/web/studio/app.ts +68 −32
@@ -88,6 +88,17 @@ function decisionWords(stats: readonly DecisionStat[]): Html {
8888 /** Below this many served deploys a median is one or two points, not a trend. */
8989 const PREVIEW_LATENCY_MIN = 3;
9090
91 /**
92 * Below this many deploys total (served and never-served together) a
93 * failure ratio is one unlucky boot, not a rate worth a red link — the
94 * app's own first couple of production deploys should never read as a
95 * statistic.
96 */
97 const FAILED_RATIO_MIN = 5;
98
99 /** The start of history, for an "ever" count rather than one week of it. */
100 const EPOCH = new Date(0).toISOString();
101
91102 /**
92103 * How fast a change has reached its preview this week, worded like the
93104 * reference instant in `servedPhrase` (`web/ui.ts`): "reaches its preview
@@ -97,17 +108,22 @@ const PREVIEW_LATENCY_MIN = 3;
97108 * each its own median, not a piece of the total that ought to add up.
98109 * When some of the week's deploys finished without ever serving, the line
99110 * says how many of how many and links to them — a clean week reads exactly
100 * as it always has, no `0 failed`, no empty clause.
111 * as it always has, no `0 failed`, no empty clause. Below `FAILED_RATIO_MIN`
112 * total deploys the ratio says nothing a person should act on, so it stays
113 * quiet rather than reading a young app's own first boot or two as a rate.
101114 */
102115 function previewLatencyWords(
103116 latency: PreviewLatency | undefined,
104117 failedHref: string,
105118 ): Html {
119 const failedTotal = latency === undefined ? 0 : latency.n + latency.failedN;
106120 const failedClause =
107 latency !== undefined && latency.failedN > 0
108 ? html`<a href="${failedHref}">${latency.failedN} of ${
109 latency.n + latency.failedN
110 } never reached a preview</a>`
121 latency !== undefined &&
122 latency.failedN > 0 &&
123 failedTotal >= FAILED_RATIO_MIN
124 ? html`<a href="${failedHref}"
125 >${latency.failedN} of ${failedTotal} never reached a preview</a
126 >`
111127 : null;
112128 const failedSuffix =
113129 failedClause === null ? "" : html` · ${failedClause}`;
@@ -179,6 +195,18 @@ export async function studioApp(
179195 const weekLatency = ownerFacts
180196 ? previewLatency(db, repo.id, week)
181197 : undefined;
198 // Whether there is any history at all — ever, not this week's — so a
199 // fresh app reads as "nothing yet" rather than a stretch of zeros and a
200 // scary ratio (#274). A quiet week on an old app still has shipped a
201 // change or taken a run at some point; only an app with neither, ever,
202 // gets the quieter line. Deploys are not part of this: they happen on
203 // their own the moment an app exists, so they say nothing about whether
204 // anyone has done anything yet.
205 const everShipped = shipped > 0 || mergedPullCount(db, repo.id, EPOCH) > 0;
206 const everRuns = ownerFacts
207 ? weekTotals.runs > 0 || agentRunTotals(db, repo.id, EPOCH).runs > 0
208 : false;
209 const hasHistory = everShipped || everRuns;
182210 const autonomy =
183211 !ownerFacts || repo.autonomy === "off"
184212 ? undefined
@@ -482,33 +510,41 @@ export async function studioApp(
482510 },
483511 );
484512 const weekCard = card(
485 !ownerFacts
486 ? html`<dl>
487 <dt>Shipped</dt>
488 <dd>${shipped} change${shipped === 1 ? "" : "s"}</dd>
489 </dl>`
490 : html`<dl>
491 <dt>Shipped</dt>
492 <dd>${shipped} change${shipped === 1 ? "" : "s"}</dd>
493 <dt>Runs</dt>
494 <dd>${weekTotals.runs}</dd>
495 <dt>Preview</dt>
496 <dd>${previewLatencyWords(weekLatency, `${base}/runs?kind=deploy`)}</dd>
497 <dt>Spent</dt>
498 <dd>
499 $${spent.toFixed(2)}${shipped > 0
500 ? html` <span class="muted"
501 >· $${(spent / shipped).toFixed(2)} per shipped change</span
502 >`
503 : null}
504 </dd>
505 ${when(
506 ledgerWeek.length > 0,
507 () =>
508 html`<dt>Decided</dt>
509 <dd>${decisionWords(ledgerWeek)}</dd>`,
510 )}
511 </dl>`,
513 !hasHistory
514 ? html`<p class="muted small">
515 Nothing to show yet — it fills in once a change ships${ownerFacts
516 ? " or the crew takes a run"
517 : ""}.
518 </p>`
519 : !ownerFacts
520 ? html`<dl>
521 <dt>Shipped</dt>
522 <dd>${shipped} change${shipped === 1 ? "" : "s"}</dd>
523 </dl>`
524 : html`<dl>
525 <dt>Shipped</dt>
526 <dd>${shipped} change${shipped === 1 ? "" : "s"}</dd>
527 <dt>Runs</dt>
528 <dd>${weekTotals.runs}</dd>
529 <dt>Preview</dt>
530 <dd>
531 ${previewLatencyWords(weekLatency, `${base}/runs?kind=deploy`)}
532 </dd>
533 <dt>Spent</dt>
534 <dd>
535 $${spent.toFixed(2)}${shipped > 0
536 ? html` <span class="muted"
537 >· $${(spent / shipped).toFixed(2)} per shipped change</span
538 >`
539 : null}
540 </dd>
541 ${when(
542 ledgerWeek.length > 0,
543 () =>
544 html`<dt>Decided</dt>
545 <dd>${decisionWords(ledgerWeek)}</dd>`,
546 )}
547 </dl>`,
512548 {
513549 head: html`${icon("activity")}
514550 <h2>This week</h2>`,
src/web/studio/words.ts +2 −5
@@ -10,16 +10,13 @@
1010
1111 import type { Database } from "bun:sqlite";
1212 import { mergeBase } from "../../git/merge.ts";
13 import {
14 STOPPED_SUMMARY,
15 type AppsRuntime,
16 type EnvStatus,
17 } from "../../apps/runtime.ts";
13 import type { AppsRuntime, EnvStatus } from "../../apps/runtime.ts";
1814 import type { AppRecord } from "../../core/apps.ts";
1915 import { getIssueById } from "../../core/issues.ts";
2016 import { getPull, pullForBranch, type Pull } from "../../core/pulls.ts";
2117 import {
2218 checkSummary,
19 STOPPED_SUMMARY,
2320 type CheckDetail,
2421 type CheckSummary,
2522 type Run,
test/apps.test.ts +7 −2
@@ -13,7 +13,7 @@ import { mkdtemp, rm } from "node:fs/promises";
1313 import { tmpdir } from "node:os";
1414 import { join } from "node:path";
1515 import { createApp, type App } from "../src/app.ts";
16 import { AppsRuntime, STOPPED_SUMMARY } from "../src/apps/runtime.ts";
16 import { AppsRuntime } from "../src/apps/runtime.ts";
1717 import { envDataDir, envDir, previewEnv } from "../src/apps/data.ts";
1818 import {
1919 enableApp,
@@ -40,7 +40,12 @@ import { getPull, type Pull } from "../src/core/pulls.ts";
4040 import { getRepoById, type Repo } from "../src/core/repos.ts";
4141 import { resolveOid } from "../src/git/merge.ts";
4242 import { repoDir } from "../src/git/repos.ts";
43 import { getRun, listRuns, type Run } from "../src/core/runs.ts";
43 import {
44 getRun,
45 listRuns,
46 STOPPED_SUMMARY,
47 type Run,
48 } from "../src/core/runs.ts";
4449 import { runWord } from "../src/web/studio/words.ts";
4550 import { createToken } from "../src/core/tokens.ts";
4651 import { createUser, type User } from "../src/core/users.ts";
test/studio.test.ts +274 −2
@@ -42,6 +42,7 @@ import {
4242 markServing,
4343 noteRunProgress,
4444 previewLatency,
45 STOPPED_SUMMARY,
4546 type Run,
4647 } from "../src/core/runs.ts";
4748 import { createUser, getUserByName, type User } from "../src/core/users.ts";
@@ -6086,6 +6087,51 @@ describe("previewLatency: the median seconds from push to serving", () => {
60866087 expect(latency.failedN).toBe(1);
60876088 expect(latency.medianSeconds).toBeUndefined();
60886089 });
6090
6091 test("a deploy the forge's own restart cancelled is never counted as failed, nor in the total it is 'of' (#255, #274)", () => {
6092 const now = Date.now() + 40_000_000;
6093 const since = new Date(now - 60_000).toISOString();
6094
6095 // A real failure: counted.
6096 const failed = createRun(app.db, { repoId: repo.id, kind: "deploy" });
6097 app.db
6098 .query("UPDATE run SET created_at = ?2 WHERE id = ?1")
6099 .run(failed.id, new Date(now).toISOString());
6100 claimRun(app.db, failed.id);
6101 finishRun(app.db, failed.id, { state: "failed", summary: "build failed" });
6102
6103 // A deploy the forge's own restart cancelled: not this commit's fault,
6104 // and not counted at all — neither as a failure nor in the total.
6105 const stopped = createRun(app.db, { repoId: repo.id, kind: "deploy" });
6106 app.db
6107 .query("UPDATE run SET created_at = ?2 WHERE id = ?1")
6108 .run(stopped.id, new Date(now + 1_000).toISOString());
6109 claimRun(app.db, stopped.id);
6110 app.db
6111 .query(
6112 "UPDATE run SET state = 'cancelled', summary = ?2, finished_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?1",
6113 )
6114 .run(stopped.id, STOPPED_SUMMARY);
6115
6116 const latency = previewLatency(app.db, repo.id, since)!;
6117 expect(latency.n).toBe(0);
6118 expect(latency.failedN).toBe(1);
6119
6120 // A person's own Cancel, carrying a different summary, still counts —
6121 // only the forge's own reason is left out.
6122 const byPerson = createRun(app.db, { repoId: repo.id, kind: "deploy" });
6123 app.db
6124 .query("UPDATE run SET created_at = ?2 WHERE id = ?1")
6125 .run(byPerson.id, new Date(now + 2_000).toISOString());
6126 claimRun(app.db, byPerson.id);
6127 app.db
6128 .query(
6129 "UPDATE run SET state = 'cancelled', summary = 'cancelled by alice', finished_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?1",
6130 )
6131 .run(byPerson.id);
6132 const withPerson = previewLatency(app.db, repo.id, since)!;
6133 expect(withPerson.failedN).toBe(2);
6134 });
60896135 });
60906136
60916137 describe("the studio's own words for how fast a change reaches its preview", () => {
@@ -6136,6 +6182,15 @@ describe("the studio's own words for how fast a change reaches its preview", ()
61366182 if (!seeded.ok) throw new Error(JSON.stringify(seeded.error));
61376183 const enabled = await enableApp(app, alice, repo, { slug: "trickle" });
61386184 if (!enabled.ok) throw new Error(String(enabled.error));
6185 // Enough history behind it that these tests exercise the preview
6186 // wording, not the day-one line (#274) — a finished agent run from
6187 // well outside this week's own window.
6188 const seedRun = createRun(app.db, { repoId: repo.id, kind: "agent" });
6189 app.db
6190 .query("UPDATE run SET created_at = ?2 WHERE id = ?1")
6191 .run(seedRun.id, new Date(Date.now() - 30 * 24 * 60 * 60_000).toISOString());
6192 claimRun(app.db, seedRun.id);
6193 finishRun(app.db, seedRun.id, { state: "ok", summary: "seed" });
61396194 });
61406195
61416196 afterAll(async () => {
@@ -6253,6 +6308,95 @@ describe("the studio's own words for how fast a change reaches its preview", ()
62536308 });
62546309 });
62556310
6311 describe("a low deploy count never reads as a failure ratio (#274)", () => {
6312 let dataDir: string;
6313 let app: App;
6314 let servers: Servers;
6315 let base: string;
6316 let alice: User;
6317 let repo: Repo;
6318 let ownerCookie: string;
6319
6320 beforeAll(async () => {
6321 dataDir = await mkdtemp(join(tmpdir(), "forge-low-count-ratio-"));
6322 app = createApp({ dataDir, port: 0 });
6323 servers = await serve(app);
6324 base = servers.url;
6325 const a = await createUser(app.db, {
6326 username: "alice",
6327 email: "[email protected]",
6328 password: "a-good-passphrase",
6329 adminIfFirst: true,
6330 });
6331 if (!a.ok) throw new Error(a.error);
6332 alice = a.value;
6333 ownerCookie = (
6334 (
6335 await fetch(`${base}/signin`, {
6336 method: "POST",
6337 body: new URLSearchParams({
6338 username: "alice",
6339 password: "a-good-passphrase",
6340 }),
6341 redirect: "manual",
6342 })
6343 ).headers.get("set-cookie") ?? ""
6344 ).split(";")[0]!;
6345 const made = await createRepo(app, alice, {
6346 name: "tworuns",
6347 visibility: "private",
6348 description: "",
6349 });
6350 if (!made.ok) throw new Error(String(made.error));
6351 repo = made.value;
6352 const seeded = await commitToBranch(app, alice, repo, {
6353 message: "Start",
6354 files: [{ path: "README.md", content: "start\n" }],
6355 });
6356 if (!seeded.ok) throw new Error(JSON.stringify(seeded.error));
6357 const enabled = await enableApp(app, alice, repo, { slug: "tworuns" });
6358 if (!enabled.ok) throw new Error(String(enabled.error));
6359
6360 // History behind it, so the full panel renders — but only two deploys
6361 // this week, both never served, neither the forge's own doing.
6362 const seedRun = createRun(app.db, { repoId: repo.id, kind: "agent" });
6363 app.db
6364 .query("UPDATE run SET created_at = ?2 WHERE id = ?1")
6365 .run(seedRun.id, new Date(Date.now() - 30 * 24 * 60 * 60_000).toISOString());
6366 claimRun(app.db, seedRun.id);
6367 finishRun(app.db, seedRun.id, { state: "ok", summary: "seed" });
6368
6369 for (let i = 0; i < 2; i++) {
6370 const failed = createRun(app.db, {
6371 repoId: repo.id,
6372 kind: "deploy",
6373 env: "production",
6374 reason: "push",
6375 });
6376 claimRun(app.db, failed.id);
6377 finishRun(app.db, failed.id, { state: "failed", summary: "build failed" });
6378 }
6379 });
6380
6381 afterAll(async () => {
6382 servers.stop();
6383 app.close();
6384 await rm(dataDir, { recursive: true, force: true });
6385 });
6386
6387 const get = (path: string) =>
6388 fetch(`${base}${path}`, {
6389 headers: { cookie: `${ownerCookie}; forge_mode=studio` },
6390 redirect: "manual",
6391 });
6392
6393 test("2 of 2 never reads as a ratio — a handful of deploys needs to be behind it first", async () => {
6394 const page = await (await get("/alice/tworuns")).text();
6395 expect(page).toContain("not enough deploys this week to say");
6396 expect(page).not.toContain("never reached a preview");
6397 });
6398 });
6399
62566400 describe("this week's runs and spend are counted by the database, not read off a page of runs", () => {
62576401 let dataDir: string;
62586402 let app: App;
@@ -6260,6 +6404,7 @@ describe("this week's runs and spend are counted by the database, not read off a
62606404 let base: string;
62616405 let alice: User;
62626406 let busy: Repo;
6407 let quiet: Repo;
62636408 let ownerCookie: string;
62646409
62656410 /** How many agent runs this week — more than `listRuns` would ever hand back. */
@@ -6308,8 +6453,19 @@ describe("this week's runs and spend are counted by the database, not read off a
63086453 const enabled = await enableApp(app, alice, made.value, { slug: name });
63096454 if (!enabled.ok) throw new Error(String(enabled.error));
63106455 if (name === "busy") busy = made.value;
6456 if (name === "quiet") quiet = made.value;
63116457 }
63126458
6459 // A real week behind "quiet", just not this one — an old finished run,
6460 // so a week with nothing in it still reads as a quiet week (#274), not
6461 // as an app with no history at all.
6462 const oldQuiet = createRun(app.db, { repoId: quiet.id, kind: "agent" });
6463 claimRun(app.db, oldQuiet.id);
6464 finishRun(app.db, oldQuiet.id, { state: "ok", summary: "long ago" });
6465 app.db
6466 .query("UPDATE run SET created_at = ?2 WHERE id = ?1")
6467 .run(oldQuiet.id, new Date(Date.now() - 30 * 24 * 60 * 60_000).toISOString());
6468
63136469 const keeper = await createAgent(app, alice, { username: "keeper" });
63146470 if (!keeper.ok) throw new Error(String(keeper.error));
63156471 expect(addToCrew(app, alice, busy, "keeper").ok).toBe(true);
@@ -6377,7 +6533,7 @@ describe("this week's runs and spend are counted by the database, not read off a
63776533 expect(page).not.toContain("$99.00");
63786534 });
63796535
6380 test("an app with no runs this week reads 0 and $0.00, never blank", async () => {
6536 test("an app with real history but no runs this week reads 0 and $0.00, never blank", async () => {
63816537 const page = await (await get("/alice/quiet")).text();
63826538 expect(page).toMatch(/Runs<\/dt>\s*<dd>0<\/dd>/);
63836539 expect(page).toContain("$0.00");
@@ -6391,6 +6547,7 @@ describe("this week's shipping is counted by the database, not read off a page o
63916547 let base: string;
63926548 let alice: User;
63936549 let busy: Repo;
6550 let quiet: Repo;
63946551 let ownerCookie: string;
63956552
63966553 /** How many changes landed this week — past the page of closed pulls the
@@ -6455,6 +6612,7 @@ describe("this week's shipping is counted by the database, not read off a page o
64556612 const enabled = await enableApp(app, alice, made.value, { slug: name });
64566613 if (!enabled.ok) throw new Error(String(enabled.error));
64576614 if (name === "busy") busy = made.value;
6615 if (name === "quiet") quiet = made.value;
64586616 }
64596617
64606618 // 150 changes landed inside the week — past the hundred closed pulls the
@@ -6470,6 +6628,15 @@ describe("this week's shipping is counted by the database, not read off a page o
64706628 new Date(now - 10 * 24 * 60 * 60_000).toISOString(),
64716629 );
64726630
6631 // A real week behind "quiet", just not this one — a change shipped a
6632 // month back, so a week with nothing new still reads as a quiet week
6633 // (#274), not as an app with no history at all.
6634 seedPull(
6635 quiet,
6636 "shipped a month ago",
6637 new Date(now - 30 * 24 * 60 * 60_000).toISOString(),
6638 );
6639
64736640 const keeper = await createAgent(app, alice, { username: "keeper" });
64746641 if (!keeper.ok) throw new Error(String(keeper.error));
64756642 expect(addToCrew(app, alice, busy, "keeper").ok).toBe(true);
@@ -6521,13 +6688,118 @@ describe("this week's shipping is counted by the database, not read off a page o
65216688 expect(page).toContain(`$${(SPENT / SHIPPED).toFixed(2)} per shipped change`);
65226689 });
65236690
6524 test("an app that shipped nothing this week reads 0 changes and no cost per change", async () => {
6691 test("an app with real history but nothing shipped this week reads 0 changes and no cost per change", async () => {
65256692 const page = await (await get("/alice/quiet")).text();
65266693 expect(page).toMatch(/Shipped<\/dt>\s*<dd>0 changes<\/dd>/);
65276694 expect(page).not.toContain("per shipped change");
65286695 });
65296696 });
65306697
6698 describe("a one-minute-old app's front door does not lead with a week of zeros (#274)", () => {
6699 let dataDir: string;
6700 let app: App;
6701 let servers: Servers;
6702 let base: string;
6703 let alice: User;
6704 let repo: Repo;
6705 let ownerCookie: string;
6706
6707 beforeAll(async () => {
6708 dataDir = await mkdtemp(join(tmpdir(), "forge-day-one-"));
6709 app = createApp({ dataDir, port: 0 });
6710 servers = await serve(app);
6711 base = servers.url;
6712 const a = await createUser(app.db, {
6713 username: "alice",
6714 email: "[email protected]",
6715 password: "a-good-passphrase",
6716 adminIfFirst: true,
6717 });
6718 if (!a.ok) throw new Error(a.error);
6719 alice = a.value;
6720 ownerCookie = (
6721 (
6722 await fetch(`${base}/signin`, {
6723 method: "POST",
6724 body: new URLSearchParams({
6725 username: "alice",
6726 password: "a-good-passphrase",
6727 }),
6728 redirect: "manual",
6729 })
6730 ).headers.get("set-cookie") ?? ""
6731 ).split(";")[0]!;
6732 const made = await createRepo(app, alice, {
6733 name: "newborn",
6734 visibility: "public",
6735 description: "",
6736 });
6737 if (!made.ok) throw new Error(String(made.error));
6738 repo = made.value;
6739 const seeded = await commitToBranch(app, alice, repo, {
6740 message: "Start",
6741 files: [{ path: "README.md", content: "start\n" }],
6742 });
6743 if (!seeded.ok) throw new Error(JSON.stringify(seeded.error));
6744 const enabled = await enableApp(app, alice, repo, { slug: "newborn" });
6745 if (!enabled.ok) throw new Error(String(enabled.error));
6746
6747 // Exactly the shape of a one-minute-old app: its own first two
6748 // production deploys, one of which the forge's own restart cancelled —
6749 // never a shipped change, never an agent run.
6750 const cancelledByForge = createRun(app.db, {
6751 repoId: repo.id,
6752 kind: "deploy",
6753 env: "production",
6754 reason: "boot",
6755 });
6756 claimRun(app.db, cancelledByForge.id);
6757 app.db
6758 .query(
6759 "UPDATE run SET state = 'cancelled', summary = ?2, finished_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?1",
6760 )
6761 .run(cancelledByForge.id, STOPPED_SUMMARY);
6762 const secondBoot = createRun(app.db, {
6763 repoId: repo.id,
6764 kind: "deploy",
6765 env: "production",
6766 reason: "push",
6767 });
6768 claimRun(app.db, secondBoot.id);
6769 });
6770
6771 afterAll(async () => {
6772 servers.stop();
6773 app.close();
6774 await rm(dataDir, { recursive: true, force: true });
6775 });
6776
6777 const get = (path: string) =>
6778 fetch(`${base}${path}`, {
6779 headers: { cookie: `${ownerCookie}; forge_mode=studio` },
6780 redirect: "manual",
6781 });
6782
6783 test("the day-one page a person lands on from New app says one true line, not a week of zeros", async () => {
6784 const page = await (await get("/alice/newborn")).text();
6785 expect(page).toContain("This week");
6786 expect(page).toContain("Nothing to show yet");
6787 // Not a single zero from the old panel, and not the scary ratio: the
6788 // forge's own cancelled boot is never counted, and two deploys is well
6789 // under the threshold a ratio needs anyway.
6790 expect(page).not.toMatch(/Runs<\/dt>/);
6791 expect(page).not.toMatch(/Spent<\/dt>/);
6792 expect(page).not.toContain("never reached a preview");
6793 expect(page).not.toContain("$0.00");
6794 });
6795
6796 test("a stranger sees the same quiet line, not the owner's zeros", async () => {
6797 const page = await (await fetch(`${base}/alice/newborn`)).text();
6798 expect(page).toContain("Nothing to show yet");
6799 expect(page).not.toMatch(/Shipped<\/dt>\s*<dd>0 changes<\/dd>/);
6800 });
6801 });
6802
65316803 describe("finding things in the studio: /search", () => {
65326804 let dataDir: string;
65336805 let app: App;

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs