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

A test pins how a run reads in the terminal: each run state's tone, the environment in words and dollars to the cent #7603

Merged opened this

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

Merged

torque/forge:torque/7594-a-test-pins-how-a-run-reads-in-the-termi → main 1 commit fork 1 thread · 1 unresolved

  • Merged. torque/7594-a-test-pins-how-a-run-reads-in-the-termi was merged into main as 2660de8.

Review

Sign in to review this change.
2 files changed +144 −1
src/core/runs.ts +3 −1
@@ -25,7 +25,9 @@ export const STOPPED_SUMMARY = "the forge stopped";
2525 /** The kinds that pass judgement on a commit: a preview's deploy, or a repository's own check. */
2626 export const CHECK_KINDS: readonly RunKind[] = ["deploy", "check"];
2727 export const RUN_KINDS: readonly RunKind[] = ["deploy", "agent", "check"];
28 export type RunState = "queued" | "running" | "ok" | "failed" | "cancelled";
28 /** Every state a run can be in — the type is read off this list, so a new state is named here once. */
29 export const RUN_STATES = ["queued", "running", "ok", "failed", "cancelled"] as const;
30 export type RunState = (typeof RUN_STATES)[number];
2931
3032 export const RUN_TERMINAL: readonly RunState[] = ["ok", "failed", "cancelled"];
3133 export const isTerminal = (state: RunState): boolean =>
test/tui-runwords.test.ts added +141
@@ -0,0 +1,141 @@
1 /**
2 * How a run reads in the terminal (#7594): every state's tone, the
3 * environment in words, the dollars to the cent — and the runs list and the
4 * run log saying the same words for one run.
5 */
6 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7 import { mkdtemp, rm } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { createRepo } from "../src/actions/repos.ts";
11 import { createApp, type App as Forge } from "../src/app.ts";
12 import { RUN_STATES, claimRun, createRun, finishRun, type Run, type RunState } from "../src/core/runs.ts";
13 import type { Repo } from "../src/core/repos.ts";
14 import type { User } from "../src/core/users.ts";
15 import { createForgeApp } from "../src/tui/forge.ts";
16 import { runLogScreen } from "../src/tui/screens/runlog.ts";
17 import { runsScreen } from "../src/tui/screens/runs.ts";
18 import { costText, envLabel, runTone } from "../src/tui/screens/runwords.ts";
19 import type { Screen } from "../src/tui/app/app.ts";
20 import { headlessTerminal } from "../src/tui/term/terminal.ts";
21 import type { Tone } from "../src/tui/ui/widgets.ts";
22 import { seedAdmin } from "./helpers/admin.ts";
23 import { quietLogs } from "./helpers/globals.ts";
24 import { settleApp } from "./helpers/tui.ts";
25
26 quietLogs();
27
28 /**
29 * The tone each state wears. A `Record` over `RunState`, so a state added to
30 * `RUN_STATES` without a row here fails the type check, and the loop below
31 * fails the test. `queued` has no tone on purpose: nothing has happened yet.
32 */
33 const TONES: Record<RunState, Tone | undefined> = {
34 queued: undefined,
35 running: "attn",
36 ok: "success",
37 failed: "danger",
38 cancelled: "secondary",

Next: this uniqueness test is nearly tautological for a literal tuple; drop it or fold it into the loop below.

39 };
40
41 describe("runTone", () => {
42 test("RUN_STATES names each state once", () => {
43 expect(new Set(RUN_STATES).size).toBe(RUN_STATES.length);
44 expect(RUN_STATES.length).toBeGreaterThan(0);
45 });
46
47 for (const state of RUN_STATES) {
48 test(`${state} wears its tone`, () => {
49 expect(Object.hasOwn(TONES, state)).toBe(true);
50 expect(runTone(state)).toBe(TONES[state]);
51 });
52 }
53
54 test("only a queued run goes without a tone", () => {
55 expect(RUN_STATES.filter((s) => runTone(s) === undefined)).toEqual(["queued"]);
56 });
57 });
58
59 describe("envLabel", () => {
60 test("a pull's environment reads as the pull", () => {
61 expect(envLabel("pull-4")).toBe("pull #4");
62 expect(envLabel("pull-7594")).toBe("pull #7594");
63 });
64 test("a named environment reads as its name", () => {
65 expect(envLabel("production")).toBe("production");
66 });
67 test("no environment reads as a dash", () => {
68 expect(envLabel("")).toBe("—");
69 });
70 });
71
72 describe("costText", () => {
73 test("zero spent says nothing", () => {
74 expect(costText(0)).toBe("");
75 });
76 // Rounded to the cent, as the web studio cards and the runner's line do:
77 // a sub-cent run reads $0.00, not nothing, because it did spend.
78 test("sub-cent rounds to the cent", () => {
79 expect(costText(0.004)).toBe("$0.00");
80 expect(costText(0.005)).toBe("$0.01");
81 });
82 test("cents read to the cent", () => {
83 expect(costText(0.07)).toBe("$0.07");
84 expect(costText(0.4)).toBe("$0.40");
85 expect(costText(3.456)).toBe("$3.46");
86 });
87 test("thousands read whole, to the cent, with no separator", () => {
88 expect(costText(1234.5)).toBe("$1234.50");
89 expect(costText(12000)).toBe("$12000.00");
90 });
91 });
92
93 describe("the runs list and the run log read one run alike", () => {
94 let dir: string;
95 let forge: Forge;
96 let repo: Repo;
97 let alice: User;
98 let run: Run;
99
100 beforeAll(async () => {
101 dir = await mkdtemp(join(tmpdir(), "forge-tui-runwords-"));
102 forge = createApp({ dataDir: dir, port: 0 });
103 const a = await seedAdmin(forge, { username: "alice", email: "[email protected]", password: "a-long-passphrase" });
104 if (!a.ok) throw new Error("signup failed");
105 alice = a.value;
106 const r = await createRepo(forge, alice, { name: "lamp", visibility: "public" });
107 if (!r.ok) throw new Error("repo failed");
108 repo = r.value;
109 run = createRun(forge.db, { repoId: repo.id, kind: "deploy", env: "pull-4" });
110 claimRun(forge.db, run.id);
111 finishRun(forge.db, run.id, { state: "failed", summary: "LAMP-BROKE", costUsd: 1234.5 });
112 });
113 afterAll(async () => {
114 forge.close?.();
115 await rm(dir, { recursive: true, force: true });
116 });
117
118 const draw = async (screen: (f: Parameters<typeof runsScreen>[0], ctx: Parameters<typeof runsScreen>[1]) => Screen) => {
119 const { app, session } = createForgeApp(
120 forge,
121 { sshUsername: "alice", remoteAddress: "127.0.0.1", user: alice },
122 { terminal: headlessTerminal(140, 20), exitHooks: false },
123 );
124 app.start();
125 await settleApp(app);
126 app.push(screen(session, app.ctx));
127 await settleApp(app);
128 const text = app.screen.toText();
129 app.stop();
130 return text;
131 };
132
133 test("both say the state, the environment and the dollars in the same words", async () => {
134 const list = await draw((f, ctx) => runsScreen(f, ctx, repo));
135 const log = await draw((f, ctx) => runLogScreen(f, ctx, repo, run.id));
136 for (const words of ["failed", "deploy pull #4", "$1234.50", "LAMP-BROKE"]) {
137 expect(list).toContain(words);
138 expect(log).toContain(words);
139 }
140 });
141 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs