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

A test pins what a park reason says about each try: the files each red head changed since the one before #6886

Merged opened this

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

Merged

scout/forge:scout/6873-a-test-pins-what-a-park-reason-says-abou → main 1 commit fork

  • Merged. scout/6873-a-test-pins-what-a-park-reason-says-abou was merged into main as dbeaa85.

Review

Sign in to review this change.
1 file changed +219 −0
test/crew-tries.test.ts added +219
@@ -0,0 +1,219 @@
1 /**
2 * What a park reason says about each try (#6873): the files each red head
3 * changed since the head before it, the first since the base. Runs are
4 * written with explicit instants; the wall clock is never read.
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 { createApp, type App } from "../src/app.ts";
11 import { openPull } from "../src/actions/pulls.ts";
12 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
13 import { trackPullHead, type Pull } from "../src/core/pulls.ts";
14 import { getRepoById, type Repo } from "../src/core/repos.ts";
15 import { claimRun, createRun, finishRun } from "../src/core/runs.ts";
16 import { createUser, type User } from "../src/core/users.ts";
17 import { resolveOid } from "../src/git/merge.ts";
18 import { repoDir } from "../src/git/repos.ts";
19 import { triesChanged } from "../src/crew/tries.ts";
20
21 const at = (n: number) => `2026-01-01T00:00:${String(n).padStart(2, "0")}.000Z`;
22 const EPOCH = at(0);
23
24 describe("triesChanged", () => {
25 let dataDir: string;
26 let app: App;
27 let alice: User;
28 let repo: Repo;
29 let indexN = 0;
30 let branchN = 0;
31
32 beforeAll(async () => {
33 dataDir = await mkdtemp(join(tmpdir(), "forge-tries-"));
34 app = createApp({ dataDir, port: 0 });
35 const a = await createUser(app.db, {
36 origin: "seed",
37 trusted: true,
38 username: "alice",
39 email: "[email protected]",
40 password: "a-good-passphrase",
41 adminIfFirst: true,
42 });
43 if (!a.ok) throw new Error(a.error);
44 alice = a.value;
45 const made = await createRepo(app, alice, { name: "engine", visibility: "private" });
46 if (!made.ok) throw new Error(String(made.error));
47 repo = made.value;
48 const seeded = await commitToBranch(app, alice, fresh(), {
49 message: "Start",
50 files: [{ path: "README.md", content: "start\n" }],
51 });
52 if (!seeded.ok) throw new Error(JSON.stringify(seeded.error));
53 });
54
55 afterAll(async () => {
56 app.close();
57 await rm(dataDir, { recursive: true, force: true });
58 });
59
60 const fresh = () => getRepoById(app.db, repo.id)!;
61
62 /** A pull whose branch holds one commit, so a pull exists to ask about. */
63 async function newPull(): Promise<{ pull: Pull; branch: string }> {
64 const branch = `feature/${++branchN}`;
65 const c = await commitToBranch(app, alice, fresh(), {
66 branch,
67 base: "main",
68 message: "First",
69 files: [{ path: `first-${branchN}.txt`, content: "1\n" }],
70 });
71 if (!c.ok) throw new Error(JSON.stringify(c.error));
72 const opened = await openPull(app, alice, fresh(), {
73 title: "Tries",
74 body: "",
75 baseBranch: "main",
76 headBranch: branch,
77 });
78 if (!opened.ok) throw new Error(String(opened.error));
79 return { pull: opened.value, branch };
80 }
81
82 /** Commit on the branch; the new head's oid. */
83 async function commit(branch: string, files: { path: string; content: string }[]) {
84 const c = await commitToBranch(app, alice, fresh(), { branch, message: "Try", files });
85 if (!c.ok) throw new Error(JSON.stringify(c.error));
86 return c.value.commitOid;
87 }
88
89 /** A finished check run on `oid`, created at second `n`. */
90 function run(p: Pull, oid: string, state: "ok" | "failed", n: number, kind: "check" | "deploy" = "check") {
91 const r = createRun(app.db, { repoId: repo.id, kind, issueId: p.id, commitOid: oid });
92 claimRun(app.db, r.id);
93 finishRun(app.db, r.id, { state, summary: state });
94 app.db.query("UPDATE run SET created_at = ?2 WHERE id = ?1").run(r.id, at(n));
95 return r;
96 }
97
98 /**
99 * A commit adding one file whose name the API refuses (a newline), made
100 * with git directly on top of the branch tip, the way a push could.
101 */
102 async function pushed(branch: string, path: string): Promise<string> {
103 const bare = repoDir(app.reposRoot, repo.ownerName, repo.name);
104 const index = join(dataDir, `index-${++indexN}`);
105 const git = async (args: string[], stdin?: string) => {
106 const proc = Bun.spawn(["git", ...args], {
107 cwd: bare,
108 stdin: stdin === undefined ? "ignore" : new Blob([stdin]),
109 stdout: "pipe",
110 stderr: "pipe",
111 env: {
112 ...process.env,
113 GIT_INDEX_FILE: index,
114 GIT_AUTHOR_NAME: "t",
115 GIT_AUTHOR_EMAIL: "[email protected]",
116 GIT_COMMITTER_NAME: "t",
117 GIT_COMMITTER_EMAIL: "[email protected]",
118 },
119 });
120 const out = await new Response(proc.stdout).text();
121 expect(await proc.exited).toBe(0);
122 return out.trim();
123 };
124 const tip = (await resolveOid(bare, `refs/heads/${branch}`))!;
125 await git(["read-tree", tip]);
126 const blob = await git(["hash-object", "-w", "--stdin"], "x\n");
127 await git(["update-index", "--add", "--cacheinfo", `100644,${blob},${path}`]);
128 const tree = await git(["write-tree"]);
129 const oid = await git(["commit-tree", tree, "-p", tip, "-m", "Odd"]);
130 await git(["update-ref", `refs/heads/${branch}`, oid]);
131 trackPullHead(app.db, repo.id, branch, oid);
132 return oid;
133 }
134
135 test("tells each red head's files since the head before it, the first since the base", async () => {
136 const { pull: p, branch } = await newPull();
137 const h1 = await commit(branch, [{ path: "src/a.ts", content: "a\n" }]);
138 const h2 = await commit(branch, [{ path: "src/b.ts", content: "b\n" }]);
139 const h3 = await commit(branch, [{ path: "src/a.ts", content: "a2\n" }, { path: "src/c.ts", content: "c\n" }]);
140 run(p, h1, "failed", 1);
141 run(p, h2, "failed", 2, "deploy");
142 run(p, h3, "failed", 3);
143 const all = await triesChanged(app, fresh(), p, EPOCH);
144 // Only the last TRIES_TOLD (2) lines come back, numbered by their place.
145 expect(all).toEqual(["try 2: src/b.ts", "try 3: src/a.ts, src/c.ts"]);
146
147 // With one try the first line says the files since the base.
148 const { pull: q, branch: qb } = await newPull();
149 const g1 = await commit(qb, [{ path: "src/x.ts", content: "x\n" }]);
150 run(q, g1, "failed", 1);
151 expect(await triesChanged(app, fresh(), q, EPOCH)).toEqual([
152 `try 1: first-${branchN}.txt, src/x.ts`,
153 ]);
154 });
155
156 test("skips a green run, a run before since, a repeated head and a non-oid head", async () => {
157 const { pull: p, branch } = await newPull();
158 const old = await commit(branch, [{ path: "old.ts", content: "o\n" }]);
159 const green = await commit(branch, [{ path: "green.ts", content: "g\n" }]);
160 const red = await commit(branch, [{ path: "red.ts", content: "r\n" }]);
161 run(p, old, "failed", 1);
162 run(p, green, "ok", 5);
163 run(p, red, "failed", 6);
164 run(p, red, "failed", 7);
165 run(p, "not-an-oid", "failed", 8);
166 run(p, "", "failed", 9);
167 expect(await triesChanged(app, fresh(), p, at(2))).toEqual([`try 1: first-${branchN}.txt, green.ts, old.ts, red.ts`]);
168 expect(await triesChanged(app, fresh(), p, at(9))).toEqual([]);
169 });
170
171 test("a head that changed nothing says so", async () => {
172 const { pull: p, branch } = await newPull();
173 const h1 = await commit(branch, [{ path: "same.ts", content: "s\n" }]);
174 // The same tree again: a new commit with no file changed.
175 const h2 = await commit(branch, [{ path: "same.ts", content: "s\n" }]);
176 expect(h2).not.toBe(h1);
177 run(p, h1, "failed", 1);
178 run(p, h2, "failed", 2);
179 expect(await triesChanged(app, fresh(), p, EPOCH)).toEqual([
180 `try 1: first-${branchN}.txt, same.ts`,
181 "try 2: no files changed",
182 ]);
183 });
184
185 test("more files than the limit end in a count; a long line is cut with an ellipsis", async () => {
186 const { pull: p, branch } = await newPull();
187 const many = await commit(
188 branch,
189 ["a", "b", "c", "d", "e", "f"].map((n) => ({ path: `m/${n}.ts`, content: `${n}\n` })),
190 );
191 run(p, many, "failed", 1);
192 expect(await triesChanged(app, fresh(), p, EPOCH)).toEqual([
193 `try 1: first-${branchN}.txt, m/a.ts, m/b.ts, m/c.ts (+3 more)`,
194 ]);
195
196 const long = "d".repeat(100);
197 const wide = await commit(branch, [
198 { path: `${long}/one.ts`, content: "1\n" },
199 { path: `${long}/two.ts`, content: "2\n" },
200 ]);
201 run(p, wide, "failed", 2);
202 const lines = await triesChanged(app, fresh(), p, EPOCH);
203 const cut = lines[1]!;
204 expect(cut.length).toBe(160);
205 expect(cut.endsWith("…")).toBe(true);
206 expect(cut.startsWith(`try 2: ${long}/one.ts, `)).toBe(true);
207 });
208
209 test("a name with a newline or markup comes back as git gave it, as text", async () => {
210 const { pull: p, branch } = await newPull();
211 const odd = "we\nird/<img src=x onerror=alert(1)>.ts";
212 const h = await pushed(branch, odd);
213 run(p, h, "failed", 1);
214 const lines = await triesChanged(app, fresh(), p, EPOCH);
215 expect(lines).toEqual([`try 1: first-${branchN}.txt, ${odd}`]);
216 expect(lines[0]).toContain("<img src=x onerror=alert(1)>");
217 expect(lines[0]).not.toContain("&lt;");
218 });
219 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs