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

The app page and the pull page show the same included-credit figure as the home #11716

Merged opened this

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

Merged

scout/forge:scout/11615-the-app-page-and-the-pull-page-show-the → main 3 commits fork 3 threads · 3 unresolved

  • Merged. scout/11615-the-app-page-and-the-pull-page-show-the was merged into main as eb13b5d.

Review

Sign in to review this change.
4 files changed +118 −4
src/tui/screens/crewkey.ts +1 −1
@@ -24,7 +24,7 @@ export function crewKeyLine(f: Session): string {
2424 if (crewHasCredential(f.app.db)) return "The crew runs on your key. a adds another.";
2525 const credit = includedCredit(f.app);
2626 if (credit !== undefined && credit.leftUsd > 0)
27 return `The crew is building on ${usdWords(credit.leftUsd)} of ${usdWords(credit.capUsd)} included credit. a adds your own key.`;
27 return `The crew is building on ${usdWords(credit.leftUsd)} of ${usdWords(credit.capUsd)} included credit (${usdWords(credit.spentUsd)} spent). a adds your own key.`;
2828 if (credit !== undefined) return "The included credit is spent. a adds your own key to keep building.";
2929 return "The crew needs a model key to start. a adds yours.";
3030 }
src/web/studio/app.ts +10 −2
@@ -63,7 +63,8 @@ import { adapterCard } from "./adapter.ts";
6363 import { schedulesCard } from "./schedules.ts";
6464 import { appPanels } from "./previews.ts";
6565 import { noCrewLine, pausedWords, servingHistory, type WordCtx } from "./words.ts";
66 import { crewKeyCard } from "./crew_key.ts";
66 import { crewKeyCard, includedCredit } from "./crew_key.ts";
67 import { usdWords } from "../../spawn/credit.ts";
6768 import {
6869 notePhase,
6970 takeSessionTime,
@@ -297,6 +298,7 @@ export async function studioApp(
297298 ? timePhase(req, "spend", () => agentRunTotals(db, repo.id, week))
298299 : { runs: 0, spentUsd: 0 };
299300 const spent = weekTotals.spentUsd;
301 const credit = includedCredit(ctx.app);
300302 const ledgerWeek = ownerFacts && !fresh
301303 ? timePhase(req, "decisions", () => decisionStats(db, repo.id, week))
302304 : [];
@@ -395,7 +397,13 @@ export async function studioApp(
395397 <dd>
396398 ${previewLatencyWords(weekLatency, `${base}/runs?kind=deploy`)}
397399 </dd>
398 <dt>Spent</dt>
400 ${credit === undefined
401 ? null
402 : html`<dt>Included credit</dt>
403 <dd id="app-credit">
404 ${usdWords(credit.leftUsd)} of ${usdWords(credit.capUsd)} left
405 </dd>`}
406 <dt>Runs cost</dt>

Renaming Spent to Runs cost makes test/studio-shipping.test.ts:2137 (not.toMatch /Spent<\/dt>/) pass vacuously; point it at the new label.

399407 <dd>
400408 $${spent.toFixed(2)}${shipped > 0
401409 ? html` <span class="muted"
test/credit-one-figure.test.ts added +106
@@ -0,0 +1,106 @@
1 /**
2 * One included-credit figure on every surface (#11615): a forge with $2 of
3 * credit and $0.48 spent says "$1.52" on the home line, the app page, the
4 * pull page and in the terminal, and no other credit figure.
5 */
6 import { afterAll, beforeAll, expect, test } from "bun:test";
7 import { mkdtemp, rm, writeFile } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { createApp, type App } from "../src/app.ts";
11 import { createSession } from "../src/core/sessions.ts";
12 import { deleteSubscription, listSubscriptions } from "../src/core/subscriptions.ts";
13 import { getRepo } from "../src/core/repos.ts";
14 import { claimRun, createRun, finishRun } from "../src/core/runs.ts";
15 import { openPull } from "../src/actions/pulls.ts";
16 import { commitToBranch } from "../src/actions/repos.ts";
17 import { getUserByName } from "../src/core/users.ts";
18 import { serve, type Servers } from "../src/http/server.ts";
19 import { PREVIEW_ADMIN, PREVIEW_APP, seedPreview } from "../src/spawn/preview-seed.ts";
20 import { keepFunding } from "../src/spawn/funding.ts";
21 import { crewKeyLine } from "../src/tui/screens/crewkey.ts";
22 import { crewFirstLine } from "../src/web/studio/crew_key.ts";
23 import { quietLogs } from "./helpers/globals.ts";
24
25 quietLogs();
26
27 let dataDir = "";
28 let app: App;
29 let servers: Servers;
30 let cookie = "";
31 let number = 0;
32
33 beforeAll(async () => {
34 dataDir = await mkdtemp(join(tmpdir(), "forge-credit-one-"));
35 app = createApp({ dataDir, port: 0 });
36 // The sample app, with a landed pull: an app with a past, so its page shows the week.
37 const seeded = await seedPreview(app, { FORGE_APP_ENV: "preview" });
38 if (!seeded.ok) throw new Error(seeded.error);
39 // The host's credit is heard after the seed, which checks a newcomer's home without one.
40 await writeFile(join(dataDir, "parent.json"), "{}");
41 keepFunding(dataDir, { token: "maria.bearer-token", capUsd: 2, spentUsd: 0.48 });
42 // The seed gives the forge a key of its own; this forge runs on the included credit.
43 for (const sub of listSubscriptions(app.db)) deleteSubscription(app.db, sub.id);
44 servers = await serve(app);
45 cookie = `forge_session=${createSession(app.db, getUserByName(app.db, PREVIEW_ADMIN)!.id).token}`;
46 const repo = getRepo(app.db, PREVIEW_ADMIN, PREVIEW_APP)!;
47 const admin = getUserByName(app.db, PREVIEW_ADMIN)!;
48 // The app has a past: a run that cost $0.75, which the app page may show as that run's cost and never as the credit.
49 const run = createRun(app.db, { repoId: repo.id, kind: "agent", actorId: admin.id, reason: "seed" });
50 claimRun(app.db, run.id);
51 finishRun(app.db, run.id, { state: "ok", summary: "A run", costUsd: 0.75 });
52 for (const [branch, file] of [[undefined, "forge.json"], ["topic", "t.txt"]] as const) {
53 const done = await commitToBranch(app, admin, repo, {
54 ...(branch === undefined ? {} : { branch }),
55 message: "change",
56 files: [{ path: file, content: file === "forge.json" ? "{}" : "x" }],
57 });
58 if (!done.ok) throw new Error(JSON.stringify(done.error));
59 }
60 const pull = await openPull(app, admin, repo, { title: "t", body: "", baseBranch: repo.defaultBranch, headBranch: "topic" });
61 if (!pull.ok) throw new Error(JSON.stringify(pull.error));
62 number = pull.value.number;
63 }, 60_000);
64
65 afterAll(async () => {
66 await servers.stop();
67 app.close();
68 await rm(dataDir, { recursive: true, force: true });
69 });
70
71 const get = async (path: string): Promise<string> => (await fetch(`${servers.url}${path}`, { headers: { cookie } })).text();
72
73 /** Every dollar figure in a sentence of credit: what a page says about the included credit, wherever it says it. */
74 const creditSentences = (page: string): string[] =>
75 [...page.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").matchAll(/[^.<>]*included credit[^.]*\./g)].map((m) => m[0]!);
76 const figures = (text: string): string[] => [...text.matchAll(/\$\d+(?:\.\d+)?/g)].map((m) => m[0]!);
77
78 test("the home line and the app page say $1.52 of $2 and no other credit figure", async () => {
79 // The home line is what the next-steps card prints: render it with the function the home calls.
80 const home = String(crewFirstLine(app, true));
81 expect(figures(home)).toEqual(["$1.52", "$2"]);
82 expect(home).toContain("$1.52 of $2 included credit");
83
84 const page = await get(`/${PREVIEW_ADMIN}/${PREVIEW_APP}`);
outdated

Box 3: the home line is not rendered; crewNote(app) is the same string by assumption, not by test. Render the home and check it, and check the whole page for stray credit figures, not only inside the matched row.

85 expect(page).toContain("$1.52");
86 const sentences = creditSentences(page).concat(creditSentences(page.replace(/Included credit<\/dt>/, "included credit.")));
87 expect(sentences.length).toBeGreaterThan(0);
88 for (const sentence of sentences) for (const f of figures(sentence)) expect(["$1.52", "$2"]).toContain(f);
89 const row = page.match(/id="app-credit">([\s\S]*?)<\/dd>/);
90 expect(row).not.toBeNull();
91 expect(figures(row![1]!)).toEqual(["$1.52", "$2"]);
92 // The spend is never offered as the credit: $0.48 appears nowhere, and the run's cost is labelled as the runs'.
93 expect(page).not.toContain("$0.48");
94 expect(page).toMatch(/<dt>Runs cost<\/dt>\s*<dd>\s*\$0\.75/);
95 }, 30_000);
96
97 test("the pull page carries no credit figure of its own, and none that contradicts the home", async () => {
98 const pull = await get(`/${PREVIEW_ADMIN}/${PREVIEW_APP}/pulls/${number}`);
99 expect(pull).not.toContain("$0.48");
100 expect(pull).not.toContain("included credit");
101 });
102
103 test("the terminal line says the same left figure", () => {
104 const line = crewKeyLine({ app, user: undefined } as never);
105 expect(line).toContain("$1.52 of $2 included credit");
106 });
test/studio-shipping.test.ts +1 −1
@@ -2134,7 +2134,7 @@ describe("a one-minute-old app's front door does not lead with a week of zeros (
21342134 // forge's own cancelled boot is never counted, and two deploys is well
21352135 // under the threshold a ratio needs anyway.
21362136 expect(page).not.toMatch(/Runs<\/dt>/);
2137 expect(page).not.toMatch(/Spent<\/dt>/);
2137 expect(page).not.toMatch(/Runs cost<\/dt>/);
21382138 expect(page).not.toContain("never reached a preview");
21392139 expect(page).not.toContain("$0.00");
21402140 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs