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

A commit's check in the API says how many screens tall each look is and which phone look scrolls sideways #9490

Merged opened this

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

Merged

scout/forge:scout/9487-a-commit-s-check-in-the-api-says-how-man → main 2 commits fork

  • Merged. scout/9487-a-commit-s-check-in-the-api-says-how-man was merged into main as c6e0b6e.

Review

Sign in to review this change.
4 files changed +154 −2
docs/features/vitals-screens.md +2
@@ -6,3 +6,5 @@ A check's per-look vitals say how many screens tall each look is (`scrollHeight
66 - Terminal: the checks line carries the same, `vitals home: LCP …, 2.4 screens; phone: …, 1.1 screens · phone /x scrolls sideways` (`vitalsLine`, `src/checks/progress.ts`).
77
88 Report-only: neither can turn a check red. Skipped looks show no screens.
9
10 API: `GET /api/v1/repos/:owner/:name/commits/:oid/check` and `GET /api/v1/repos/:owner/:name/pulls/:number/checks` carry `looks: [{look, screens, sideways}]`, from the same file as the run page (a run with no file of its own falls back to the commit's filed looks). `screens` is the one-decimal string (`""` without a size), `sideways` is a boolean; `[]` when there are none or the reader may not read runs.
src/actions/runs.ts +24
@@ -9,6 +9,8 @@
99 */
1010
1111 import { commitVerdicts, isCommitId, releaseCheck, type CommitCheck } from "../checks/commit_check.ts";
12 import { readLooks } from "../checks/looks.ts";
13 import { vitalsRows } from "../checks/summary.ts";
1214 import { hasCommit, isAncestor } from "../git/local.ts";
1315 import { repoDir } from "../git/repos.ts";
1416 import { mayViewId } from "../core/sealed.ts";
@@ -241,6 +243,28 @@ export async function checkOfCommit(
241243 );
242244 }
243245
246 /** One look of a check as the API words it: how many screens tall, and whether a phone look scrolls sideways. */
247 export interface LookFigures {
248 readonly look: string;
249 readonly screens: string;
250 readonly sideways: boolean;
251 }
252
253 /**
254 * The looks of the run that checked a commit, from the same file the run page
255 * reads; `[]` for someone who may not read runs or when none were filed.
256 */
257 export function looksOfCheck(
258 app: App,
259 actor: User | undefined,
260 repo: Repo,
261 run: { readonly id: string; readonly commitOid: string },
262 ): LookFigures[] {
263 if (!mayReadRuns(app, actor, repo)) return [];
264 const looks = readLooks(app.config.dataDir, { id: run.id, repoId: repo.id, commitOid: run.commitOid });
265 return vitalsRows(looks).map((g) => ({ look: g.look, screens: g.screens, sideways: g.sideways }));
266 }
267
244268 /** The own green or red verdicts of a page of commits, for whoever may read the runs; empty otherwise. */
245269 export function verdictsOfCommits(
246270 app: App,
src/http/api.ts +15 −2
@@ -223,7 +223,7 @@ import {
223223 } from "../actions/crew.ts";
224224 import { assignIssue, editIssue, TAKEN_UP } from "../actions/issues.ts";
225225 import { requireReader } from "../actions/access.ts";
226 import { mayReadRuns, runsViewKey, steerRepoRun } from "../actions/runs.ts";
226 import { looksOfCheck, mayReadRuns, runsViewKey, steerRepoRun } from "../actions/runs.ts";
227227 import { openPlan, parsePlan, proposePlan } from "../actions/plan.ts";
228228 import { repoGraph } from "../core/dependencies.ts";
229229 import { mapHeldFor } from "../crew/held.ts";
@@ -1877,7 +1877,17 @@ export function apiRoutes(router: Router<Handler>): void {
18771877 );
18781878 if (!found.ok) return notFound();
18791879 return timePhase(ctx.request, "render", () =>
1880 json(found.value, 200, { "cache-control": "no-store" }),
1880 json(
1881 {
1882 ...found.value,
1883 looks: looksOfCheck(ctx.app, ctx.auth.user, repo, {
1884 id: found.value.runId ?? "",
1885 commitOid: found.value.checkedCommit,
1886 }),
1887 },
1888 200,
1889 { "cache-control": "no-store" },
1890 ),
18811891 );
18821892 });
18831893
@@ -4033,6 +4043,9 @@ export function apiRoutes(router: Router<Handler>): void {
40334043 state: latest?.state ?? "none",
40344044 required: found.repo.requireChecks,
40354045 run: latest ?? null,
4046 looks: latest
4047 ? looksOfCheck(ctx.app, ctx.auth.user, found.repo, { id: latest.id, commitOid: latest.commitOid })
4048 : [],
40364049 runs,
40374050 }),
40384051 );
test/check-looks-api.test.ts added +113
@@ -0,0 +1,113 @@
1 /** The check's looks in the API: screens and sideways, without the run page (#9487). */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdirSync, writeFileSync } from "node:fs";
4 import { mkdtemp, rm } from "node:fs/promises";
5 import { tmpdir } from "node:os";
6 import { join } from "node:path";
7 import { createApp, type App } from "../src/app.ts";
8 import { openPull } from "../src/actions/pulls.ts";
9 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
10 import { looksPath } from "../src/checks/looks.ts";
11 import { claimRun, createRun, finishRun } from "../src/core/runs.ts";
12 import { createSession } from "../src/core/sessions.ts";
13 import { createUser } from "../src/core/users.ts";
14 import { serve, type Servers } from "../src/http/server.ts";
15 import { quietLogs } from "./helpers/globals.ts";
16
17 quietLogs();
18
19 let dataDir: string;
20 let app: App;
21 let servers: Servers;
22 let cookie = "";
23 let repoId = "";
24 let pullId = "";
25 let number = 0;
26 const cells = process.env["FORGE_CELLS"];
27 const OID = "b".repeat(40);
28
29 const get = async (path: string) => {
30 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
31 return (await res.json()) as any;
32 };
33
34 const writeLooks = (runId: string, size: object) => {
35 const path = looksPath(dataDir, runId);
36 mkdirSync(join(path, ".."), { recursive: true });
37 writeFileSync(
38 path,
39 JSON.stringify([
40 { look: "phone /x", entries: {}, size },
41 { look: "desktop", entries: {} },
42 ]),
43 );
44 };
45
46 beforeAll(async () => {
47 process.env["FORGE_CELLS"] = "off";
48 dataDir = await mkdtemp(join(tmpdir(), "forge-check-looks-api-"));
49 app = createApp({ dataDir, port: 0 });
50 servers = await serve(app);
51 const made = await createUser(app.db, {
52 origin: "seed",
53 trusted: true,
54 username: "alice",
55 email: "[email protected]",
56 password: "a-good-passphrase",
57 adminIfFirst: true,
58 });
59 if (!made.ok) throw new Error(String(made.error));
60 const alice = made.value;
61 cookie = `forge_session=${createSession(app.db, alice.id).token}`;
62 const repo = await createRepo(app, alice, { name: "r", visibility: "public" });
63 if (!repo.ok) throw new Error(String(repo.error));
64 repoId = repo.value.id;
65 for (const [branch, file] of [[undefined, "forge.json"], ["topic", "t.txt"]] as const) {
66 const done = await commitToBranch(app, alice, repo.value, {
67 ...(branch === undefined ? {} : { branch }),
68 message: "change",
69 files: [{ path: file, content: file === "forge.json" ? "{}" : "x" }],
70 });
71 if (!done.ok) throw new Error(JSON.stringify(done.error));
72 }
73 const pull = await openPull(app, alice, repo.value, { title: "t", body: "", baseBranch: "main", headBranch: "topic" });
74 if (!pull.ok) throw new Error(JSON.stringify(pull.error));
75 number = pull.value.number;
76 pullId = pull.value.id;
77 });
78
79 afterAll(async () => {
80 await servers.stop();
81 app.close();
82 if (cells === undefined) delete process.env["FORGE_CELLS"];
83 else process.env["FORGE_CELLS"] = cells;
84 await rm(dataDir, { recursive: true, force: true });
85 });
86
87 test("the commit's check carries screens and sideways; [] without a file", async () => {
88 const run = createRun(app.db, { repoId, kind: "check", commitOid: OID, env: "main", branch: "main" });
89 claimRun(app.db, run.id);
90 finishRun(app.db, run.id, { state: "failed", summary: "x" });
91 expect((await get(`/api/v1/repos/alice/r/commits/${OID}/check`)).looks).toEqual([]);
92 writeLooks(run.id, { scrollWidth: 500, scrollHeight: 2000, innerWidth: 390, innerHeight: 800 });
93 const body = await get(`/api/v1/repos/alice/r/commits/${OID}/check`);
94 expect(body.runId).toBe(run.id);
95 expect(body.looks).toEqual([
96 { look: "phone /x", screens: "2.5", sideways: true },
97 { look: "desktop", screens: "", sideways: false },
98 ]);
99 });
100
101 test("the pull's checks carry the same looks beside run", async () => {
102 const head = (await get(`/api/v1/repos/alice/r/pulls/${number}/checks`)).head as string;
103 expect((await get(`/api/v1/repos/alice/r/pulls/${number}/checks`)).looks).toEqual([]);
104 const run = createRun(app.db, { repoId, kind: "check", issueId: pullId, commitOid: head, branch: "topic" });
105 expect((await get(`/api/v1/repos/alice/r/pulls/${number}/checks`)).looks).toEqual([]);
106 writeLooks(run.id, { scrollWidth: 390, scrollHeight: 1000, innerWidth: 390, innerHeight: 800 });
107 const body = await get(`/api/v1/repos/alice/r/pulls/${number}/checks`);
108 expect(body.run.id).toBe(run.id);
109 expect(body.looks).toEqual([
110 { look: "phone /x", screens: "1.3", sideways: false },
111 { look: "desktop", screens: "", sideways: false },
112 ]);
113 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs