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

The OG card for a run names its agent, its issue, its cost and how it ended #2059

Merged opened this

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

Merged

scout/forge:scout/2046-the-og-card-for-a-run-names-its-agent-it → main 2 commits fork 4 threads · 4 unresolved

  • Merged. scout/2046-the-og-card-for-a-run-names-its-agent-it was merged into main as 9090d1b.

Review

Sign in to review this change.
2 files changed +81 −4
src/web/pages/og.ts +30 −3
@@ -15,6 +15,8 @@
1515 import { createHash } from "node:crypto";
1616 import { join } from "node:path";
1717 import type { App } from "../../app.ts";
18 import type { Run } from "../../core/runs.ts";
19 import { getUserById } from "../../core/users.ts";
1820 import { getRepoRun } from "../../actions/runs.ts";
1921 import { getAppBySlug, isSlug } from "../../core/apps.ts";
2022 import {
@@ -25,7 +27,7 @@ import {
2527 type Repo,
2628 } from "../../core/repos.ts";
2729 import { dependenciesOf, repoGraph, readyNow } from "../../core/dependencies.ts";
28 import { getIssue, type Issue } from "../../core/issues.ts";
30 import { getIssue, getIssueById, type Issue } from "../../core/issues.ts";
2931 import { getPull, type Pull } from "../../core/pulls.ts";
3032 import { mayView } from "../../core/sealed.ts";
3133 import { isSafeName } from "../../git/guard.ts";
@@ -132,17 +134,42 @@ function threadInput(ctx: Ctx, kind: OgKind, ref: string): CardInput | undefined
132134 };
133135 }
134136
137 /** The outcome of an agent run in a word, read from its state and last words. */
138 function outcome(run: Run): string {
139 if (run.state === "queued") return "queued";
140 if (run.state === "running") return "working";
141 if (run.state === "cancelled") return "cancelled";
142 if (/opened pull #|updated pull #/.test(run.summary)) return "landed a pull";

Summary-regex outcome breaks silently if the wording changes. Next: a structured outcome field on the run.

143 if (/\bdeclined:/i.test(run.summary)) return "declined";
144 return run.state === "ok" ? "done" : "failed";
145 }
146
135147 function runInput(ctx: Ctx, ref: string): CardInput | undefined {
136148 const at = threePart(ctx, ref);
137149 if (!at || !RUN_ID.test(at.rest)) return undefined;
150 // getRepoRun already seals: a run on a sealed thread is the same 404.
138151 const found = getRepoRun(ctx.app, undefined, at.repo, at.rest);
139152 if (!found.ok) return undefined;

Unreachable guard: getRepoRun already 404s runs whose issue the anonymous reader may not view. Drop mayView here and its import.

140153 const run = found.value;
154 const db = ctx.app.db;
155 const who =
156 run.actorId === null ? undefined : getUserById(db, run.actorId)?.username;
157 const repoName = `${at.repo.ownerName}/${at.repo.name}`;
158 if (run.kind === "agent") {
159 const about = run.issueId === null ? undefined : getIssueById(db, run.issueId);
160 const thread = about ? `#${about.number} ${about.title}` : undefined;
161 return {
162 kind: "run",
163 title: run.summary || "agent run",
164 lines: [repoName, ...(thread ? [thread] : [])],
165 stat: `${who ?? "agent"} · ${outcome(run)} · $${run.costUsd.toFixed(2)}`,
166 };
167 }
141168 return {
142169 kind: "run",
143170 title: run.summary || `${run.kind} run`,
144 lines: [`${at.repo.ownerName}/${at.repo.name}`],
145 stat: `${run.kind} · ${run.state}`,
171 lines: [repoName, ...(who ? [`by ${who}`] : [])],
172 stat: `${run.kind}${run.env ? ` · ${run.env}` : ""} · ${run.state}`,
146173 };
147174 }
148175
test/og-route.test.ts +51 −1
@@ -9,7 +9,7 @@ import { createApp } from "../src/core/apps.ts";
99 import { addDependency } from "../src/core/dependencies.ts";
1010 import { getIssue, createIssue } from "../src/core/issues.ts";
1111 import { createRun } from "../src/core/runs.ts";
12 import { createUser } from "../src/core/users.ts";
12 import { createUser, getUserByName } from "../src/core/users.ts";
1313 import { serve, type Servers } from "../src/http/server.ts";
1414
1515 describe("og cards", () => {
@@ -207,6 +207,56 @@ describe("og cards", () => {
207207 }
208208 });
209209
210 test("an agent run names its agent, thread, cost and outcome", async () => {
211 const repo = getRepo(app.db, "alice", "shown")!;
212 const alice = getUserByName(app.db, "alice")!;
213 const issue = getIssue(app.db, repo.id, 1)!;
214 const r = createRun(app.db, {
215 repoId: repo.id,
216 kind: "agent",
217 issueId: issue.id,
218 actorId: alice.id,
219 summary: "opened pull #2 on alice/shown",
220 });
221 app.db.query("UPDATE run SET state = 'ok', cost_usd = 1.234 WHERE id = ?1").run(r.id);
222 const body = await (await get(`/og/run/alice~shown~${r.id}.svg`)).text();
223 expect(body).toContain("alice");
224 expect(body).toContain("#1 A public issue <b>");
225 expect(body).toContain("$1.23");
226 expect(body).toContain("landed a pull");
227 app.db.query("UPDATE run SET summary = 'declined: too big', state = 'ok' WHERE id = ?1").run(r.id);
228 expect(await (await get(`/og/run/alice~shown~${r.id}.svg`)).text()).toContain("declined ·");
229 app.db.query("UPDATE run SET state = 'failed', summary = 'boom' WHERE id = ?1").run(r.id);
230 expect(await (await get(`/og/run/alice~shown~${r.id}.svg`)).text()).toContain("failed");
231 app.db.query("UPDATE run SET state = 'running' WHERE id = ?1").run(r.id);
232 expect(await (await get(`/og/run/alice~shown~${r.id}.svg`)).text()).toContain("working");
233 });
234
235 test("a deploy run shows its environment and trigger, no cost", async () => {
236 const repo = getRepo(app.db, "alice", "shown")!;
237 const alice = getUserByName(app.db, "alice")!;
238 const r = createRun(app.db, {
239 repoId: repo.id,
240 kind: "deploy",
241 env: "production",
242 actorId: alice.id,
243 summary: "deployed",
244 });
245 const body = await (await get(`/og/run/alice~shown~${r.id}.svg`)).text();
246 expect(body).toContain("production");
247 expect(body).toContain("by alice");
248 expect(body).not.toContain("$");
249 });

not.toContain("$") passes for many wrong cards; assert the full stat line (deploy · production · ok).

250
251 test("a queued or cancelled run says so", async () => {
252 const repo = getRepo(app.db, "alice", "shown")!;
253 const r = createRun(app.db, { repoId: repo.id, kind: "agent", summary: "waiting" });
outdated

This passes without the change (the 404 comes from getRepoRun). Remove it or point it at something the change adds.

254 const card = async () => (await get(`/og/run/alice~shown~${r.id}.svg`)).text();
255 expect(await card()).toContain("queued");
256 app.db.query("UPDATE run SET state = 'cancelled' WHERE id = ?1").run(r.id);
257 expect(await card()).toContain("cancelled");
258 });
259
210260 test("the wrong kind, number or shape is 404", async () => {
211261 for (const path of [
212262 "issue/alice~shown~2", // a pull is not an issue card

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs