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

Sentinel: red:01m23vygyffx5hg2f5vm8r74sr was degraded 142 minutes today: trevato/forge: main is red at ea64226 — test-shards: over 45 s: test/crew.test.ts (52.5s); 8 pull checks wait for it to… #2378

Merged opened this

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

Merged

scout/forge:scout/2375-sentinel-red-01m23vygyffx5hg2f5vm8r74sr → main 3 commits fork

  • Merged. scout/2375-sentinel-red-01m23vygyffx5hg2f5vm8r74sr was merged into main as 0521277.

Review

Sign in to review this change.
5 files changed +309 −191
scripts/test-times.json +4 −1
@@ -26,7 +26,10 @@
2626 "test/crew-review.test.ts": 11.6,
2727 "test/crew-sandbox.test.ts": 5.3,
2828 "test/crew-wrapup.test.ts": 23.6,
29 "test/crew.test.ts": 16.7,
29 "test/crew-closed.test.ts": 10,
30 "test/crew-feedback.test.ts": 11,
31 "test/crew-paths.test.ts": 0.1,
32 "test/crew.test.ts": 7,
3033 "test/cross-repo-pull.test.ts": 13,
3134 "test/decide.test.ts": 3.1,
3235 "test/dependencies.test.ts": 10.8,
test/crew-closed.test.ts added +104
@@ -0,0 +1,104 @@
1 /**
2 * Closing a pull removes the agent's clone and starts nothing; a follow-up on a
3 * pull closed without a merge fails before it begins.
4 * One forge of its own, apart from crew.test.ts (the assignment).
5 */
6
7 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
8 import { existsSync } from "node:fs";
9 import type { App } from "../src/app.ts";
10 import {
11 addComment,
12 assignIssue,
13 openIssue,
14 setIssueState,
15 } from "../src/actions/issues.ts";
16 import { queueAgentRun } from "../src/crew/dispatch.ts";
17 import { workdirFor } from "../src/crew/runner.ts";
18 import { getAgentByName } from "../src/core/agents.ts";
19 import { getIssue } from "../src/core/issues.ts";
20 import { listPulls } from "../src/core/pulls.ts";
21 import type { Repo } from "../src/core/repos.ts";
22 import { listRuns, type Run } from "../src/core/runs.ts";
23 import type { User } from "../src/core/users.ts";
24 import {
25 agentRunSettled,
26 startStandInCrew,
27 stopStandInCrew,
28 type StandInCrew,
29 } from "./helpers/crew-standin.ts";
30
31 describe("the crew when a pull closes", () => {
32 let standIn: StandInCrew;
33 let dataDir: string;
34 let app: App;
35 let alice: User;
36 let bot: User;
37 let repo: Repo;
38
39 const settled = (where: (run: Run) => boolean, timeoutMs?: number) =>
40 agentRunSettled(app, repo, where, timeoutMs);
41
42 beforeAll(async () => {
43 standIn = await startStandInCrew();
44 ({ dataDir, app, alice, bot, repo } = standIn);
45 }, 30_000);
46
47 afterAll(() => stopStandInCrew(standIn));
48
49 let issueNumber = 0;
50
51 /** The assignment, run to its pull: the start the later tests build on. */
52 beforeAll(async () => {
53 const issue = openIssue(app, alice, repo, {
54 title: "Plant tulips",
55 body: "Along the fence.",
56 });
57 if (!issue.ok) throw new Error("issue not opened");
58 issueNumber = issue.value.number;
59 assignIssue(app, alice, repo, issue.value, bot);
60 const run = await settled((r) => r.reason === "assigned");
61 expect(run.state).toBe("ok");
62 }, 60_000);
63
64 test("closing the pull removes the clone; a closed issue starts nothing", async () => {
65 const pull = listPulls(app.db, repo.id, { state: "open" })[0]!;
66 const dir = workdirFor(
67 dataDir,
68 getAgentByName(app.db, "bot")!,
69 repo,
70 issueNumber,
71 );
72 expect(existsSync(dir)).toBe(true);
73 expect(setIssueState(app, alice, repo, pull, "closed").ok).toBe(true);
74 const deadline = Date.now() + 5_000;
75 while (existsSync(dir) && Date.now() < deadline) await Bun.sleep(50);
76 expect(existsSync(dir)).toBe(false);
77 const before = listRuns(app.db, repo.id, { kind: "agent" }).length;
78 expect(addComment(app, alice, repo, pull, "one more thing").ok).toBe(true);
79 await Bun.sleep(300);
80 expect(listRuns(app.db, repo.id, { kind: "agent" }).length).toBe(before);
81 }, 30_000);
82
83 test("a pull closed without a merge has nothing to continue: its follow-up fails before it begins", async () => {
84 const agent = getAgentByName(app.db, "bot")!;
85 // Closed without a merge, the proposal was declined: nothing to continue.
86 const closed = listPulls(app.db, repo.id, { state: "closed" }).find(
87 (p) => p.mergedAt === null,
88 )!;
89 expect(closed).toBeDefined();
90 const dead = queueAgentRun(
91 app,
92 repo,
93 getIssue(app.db, repo.id, closed.number)!,
94 agent,
95 "feedback",
96 alice.id,
97 )!;
98 const outcome = await settled((r) => r.id === dead.id);
99 expect(outcome.state).toBe("failed");
100 expect(outcome.summary).toBe(
101 `pull #${closed.number} was closed before the run began`,
102 );
103 }, 30_000);
104 });
test/crew-feedback.test.ts added +97
@@ -0,0 +1,97 @@
1 /**
2 * Feedback from a person resumes the stand-in session on the same branch.
3 * One forge of its own, apart from crew.test.ts (the assignment).
4 */
5
6 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7 import { readFileSync } from "node:fs";
8 import type { App } from "../src/app.ts";
9 import { addComment, assignIssue, openIssue } from "../src/actions/issues.ts";
10 import { listComments } from "../src/core/issues.ts";
11 import { listPulls } from "../src/core/pulls.ts";
12 import { resolveOid } from "../src/git/merge.ts";
13 import { repoDir } from "../src/git/repos.ts";
14 import { getForkOf, type Repo } from "../src/core/repos.ts";
15 import { listRuns, type Run } from "../src/core/runs.ts";
16 import type { User } from "../src/core/users.ts";
17 import {
18 agentRunSettled,
19 startStandInCrew,
20 stopStandInCrew,
21 type StandInCrew,
22 } from "./helpers/crew-standin.ts";
23
24 describe("the crew resumes on feedback", () => {
25 let standIn: StandInCrew;
26 let app: App;
27 let alice: User;
28 let bot: User;
29 let repo: Repo;
30 let argsFile: string;
31 let promptFile: string;
32
33 const settled = (where: (run: Run) => boolean, timeoutMs?: number) =>
34 agentRunSettled(app, repo, where, timeoutMs);
35
36 beforeAll(async () => {
37 standIn = await startStandInCrew();
38 ({ app, alice, bot, repo, argsFile, promptFile } = standIn);
39 }, 30_000);
40
41 afterAll(() => stopStandInCrew(standIn));
42
43 /** The assignment, run to its pull: the start the later tests build on. */
44 beforeAll(async () => {
45 const issue = openIssue(app, alice, repo, {
46 title: "Plant tulips",
47 body: "Along the fence.",
48 });
49 if (!issue.ok) throw new Error("issue not opened");
50 assignIssue(app, alice, repo, issue.value, bot);
51 const run = await settled((r) => r.reason === "assigned");
52 expect(run.state).toBe("ok");
53 }, 60_000);
54
55 test("feedback from a person resumes the session on the same branch", async () => {
56 const pull = listPulls(app.db, repo.id, { state: "open" })[0]!;
57 const before = listRuns(app.db, repo.id, { kind: "agent" }).length;
58 // The agent's own comment is not feedback.
59 expect(addComment(app, bot, repo, pull, "Working on it.").ok).toBe(true);
60 await Bun.sleep(300);
61 expect(listRuns(app.db, repo.id, { kind: "agent" }).length).toBe(before);
62 // A person's is.
63 expect(
64 addComment(app, alice, repo, pull, "Also plant daffodils please.").ok,
65 ).toBe(true);
66 const run = await settled((r) => r.reason === "feedback");
67 expect(run.state).toBe("ok");
68 expect(run.summary).toMatch(/^updated pull #\d+/);
69 const args = readFileSync(argsFile, "utf8");
70 expect(args).toContain("--resume\nfake-1");
71 const prompt = readFileSync(promptFile, "utf8");
72 expect(prompt).toContain("feedback on your pull");
73 expect(prompt).toContain("Also plant daffodils please.");
74 expect(prompt).not.toContain("Working on it.");
75 // The agent reported on the pull, and the branch moved.
76 const comments = listComments(app.db, pull.id).filter(
77 (c) => c.authorName === "bot",
78 );
79 expect(comments.map((c) => c.body)).toContain("Added CREW.md as asked.");
80 // The branch moved in the fork (the pull row keeps its opening snapshot;
81 // the live head is git's), and the base's snapshot of it followed.
82 const fork = getForkOf(app.db, bot.id, repo.id)!;
83 const tip = await resolveOid(
84 repoDir(app.reposRoot, fork.ownerName, fork.name),
85 `refs/heads/${pull.headBranch}`,
86 );
87 expect(tip).toBeDefined();
88 expect(tip).not.toBe(pull.headOid);
89 await Bun.sleep(300);
90 expect(
91 await resolveOid(
92 repoDir(app.reposRoot, repo.ownerName, repo.name),
93 `refs/pull/${pull.number}/head`,
94 ),
95 ).toBe(tip);
96 }, 60_000);
97 });
test/crew-paths.test.ts added +96
@@ -0,0 +1,96 @@
1 /**
2 * stripForgeDirs and spellings: how a run log names the files it touched.
3 * Split from crew.test.ts so the stand-in forge there runs apart.
4 */
5
6 import { describe, expect, test } from "bun:test";
7 import { mkdirSync, realpathSync, symlinkSync } from "node:fs";
8 import { mkdtemp, rm } from "node:fs/promises";
9 import { tmpdir } from "node:os";
10 import { join } from "node:path";
11 import { spellings, stripForgeDirs } from "../src/runs/paths.ts";
12
13 describe("stripForgeDirs", () => {
14 test("strips a known directory, leaves the rest of the path", () => {
15 expect(
16 stripForgeDirs(["/data/crew/bot/1/2"], "/data/crew/bot/1/2/src/a.ts"),
17 ).toBe("src/a.ts");
18 });
19
20 test("a line with no match passes through whole", () => {
21 expect(stripForgeDirs(["/data/crew/bot/1/2"], "/usr/bin/ssh")).toBe(
22 "/usr/bin/ssh",
23 );
24 });
25
26 test("an empty directory is dropped, not used as a prefix", () => {
27 expect(stripForgeDirs([""], "/usr/bin/ssh")).toBe("/usr/bin/ssh");
28 });
29
30 test("the directory alone reads as here, wherever it stands in the line", () => {
31 const dir = "/data/crew/bot/1/2";
32 expect(stripForgeDirs([dir], `cd ${dir} && ls ${dir}/src`)).toBe(
33 "cd . && ls src",
34 );
35 expect(stripForgeDirs([dir], `at "${dir}/a.ts":3 (${dir})`)).toBe(
36 'at "a.ts":3 (.)',
37 );
38 });
39
40 test("its text inside a longer path is left whole, never a path that was not there", () => {
41 const dir = "/var/folders/x/work";
42 // The same directory through a symlink the forge did not resolve.
43 expect(stripForgeDirs([dir], `/private${dir}/CREW.md`)).toBe(
44 `/private${dir}/CREW.md`,
45 );
46 // A sibling whose name begins the same way.
47 expect(stripForgeDirs(["/data/1/2"], "/data/1/23/a.ts /data/1/2.bak")).toBe(
48 "/data/1/23/a.ts /data/1/2.bak",
49 );
50 // And the whole one beside them still goes.
51 expect(
52 stripForgeDirs(["/data/1/2"], "/data/1/23/a.ts /data/1/2/a.ts"),
53 ).toBe("/data/1/23/a.ts a.ts");
54 });
55
56 test("a directory before a full stop that punctuates goes, and the stop stays", () => {
57 const dir = "/var/folders/x/work";
58 expect(stripForgeDirs([dir], `wrote to ${dir}.`)).toBe("wrote to .");
59 expect(stripForgeDirs([dir], `${dir}. Then ${dir}/a.ts`)).toBe(
60 ". Then a.ts",
61 );
62 // Inside brackets or quotes, before a comma, and in an ellipsis.
63 expect(stripForgeDirs([dir], `(see ${dir}.)`)).toBe("(see .)");
64 expect(stripForgeDirs([dir], `"${dir}.", it said`)).toBe('".", it said');
65 expect(stripForgeDirs([dir], `${dir}...`)).toBe("...");
66 // A name that goes on after the dot is a sibling, left whole — after
67 // one dot or two.
68 expect(stripForgeDirs([dir], `${dir}.bak is kept`)).toBe(
69 `${dir}.bak is kept`,
70 );
71 expect(stripForgeDirs([dir], `${dir}..bak is kept`)).toBe(
72 `${dir}..bak is kept`,
73 );
74 });
75
76 test("a directory reached through a symlink reads relative under either spelling", async () => {
77 const root = realpathSync(await mkdtemp(join(tmpdir(), "forge-paths-")));
78 try {
79 const real = join(root, "real");
80 const link = join(root, "link");
81 mkdirSync(real);
82 symlinkSync(real, link);
83 // The forge named the link; a child process reports the real path.
84 const dirs = spellings([link]);
85 expect(dirs).toEqual([link, real].sort((a, b) => b.length - a.length));
86 expect(stripForgeDirs(dirs, `${real}/CREW.md`)).toBe("CREW.md");
87 expect(stripForgeDirs(dirs, `${link}/CREW.md`)).toBe("CREW.md");
88 // A directory that is not there yet keeps its own spelling.
89 expect(spellings([join(root, "later"), ""])).toEqual([
90 join(root, "later"),
91 ]);
92 } finally {
93 await rm(root, { recursive: true, force: true });
94 }
95 });
96 });
test/crew.test.ts +8 −190
@@ -1,44 +1,30 @@
11 /**
22 * The crew loop with a stand-in for Claude Code (`helpers/crew-standin.ts`):
33 * one pull's life — assigned, cloned, worked, landed, fed back on, closed.
4 * The early pull is in `crew-early.test.ts`.
4 * Feedback is in `crew-feedback.test.ts`, closing in `crew-closed.test.ts`,
5 * the early pull in `crew-early.test.ts`.
56 * The rest of the loop is in the other `crew-*.test.ts` files, each on a
67 * forge of its own so they run apart and in parallel (#1798).
78 */
89
910 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
10 import {
11 existsSync,
12 mkdirSync,
13 readFileSync,
14 realpathSync,
15 symlinkSync,
16 } from "node:fs";
17 import { mkdtemp, rm } from "node:fs/promises";
18 import { tmpdir } from "node:os";
11 import { existsSync, readFileSync } from "node:fs";
1912 import { join } from "node:path";
2013 import type { App } from "../src/app.ts";
21 import {
22 addComment,
23 assignIssue,
24 openIssue,
25 setIssueState,
26 } from "../src/actions/issues.ts";
14 import { assignIssue, openIssue } from "../src/actions/issues.ts";
2715 import { sandboxAvailable } from "../src/crew/sandbox.ts";
28 import { queueAgentRun, type CrewHandle } from "../src/crew/dispatch.ts";
16 import type { CrewHandle } from "../src/crew/dispatch.ts";
2917 import { workdirFor } from "../src/crew/runner.ts";
3018 import { getAgentByName } from "../src/core/agents.ts";
31 import { getIssue, listComments } from "../src/core/issues.ts";
3219 import { listPulls } from "../src/core/pulls.ts";
3320 import { resolveOid } from "../src/git/merge.ts";
3421 import { listPaths } from "../src/git/read.ts";
3522 import { repoDir } from "../src/git/repos.ts";
3623 import { getForkOf, type Repo } from "../src/core/repos.ts";
37 import { listRuns, type Run } from "../src/core/runs.ts";
24 import type { Run } from "../src/core/runs.ts";
3825 import type { User } from "../src/core/users.ts";
3926 import type { Servers } from "../src/http/server.ts";
4027 import { readRunLog } from "../src/runs/log.ts";
41 import { spellings, stripForgeDirs } from "../src/runs/paths.ts";
4228 import {
4329 agentRunSettled,
4430 startStandInCrew,
@@ -46,91 +32,6 @@ import {
4632 type StandInCrew,
4733 } from "./helpers/crew-standin.ts";
4834
49 describe("stripForgeDirs", () => {
50 test("strips a known directory, leaves the rest of the path", () => {
51 expect(
52 stripForgeDirs(["/data/crew/bot/1/2"], "/data/crew/bot/1/2/src/a.ts"),
53 ).toBe("src/a.ts");
54 });
55
56 test("a line with no match passes through whole", () => {
57 expect(stripForgeDirs(["/data/crew/bot/1/2"], "/usr/bin/ssh")).toBe(
58 "/usr/bin/ssh",
59 );
60 });
61
62 test("an empty directory is dropped, not used as a prefix", () => {
63 expect(stripForgeDirs([""], "/usr/bin/ssh")).toBe("/usr/bin/ssh");
64 });
65
66 test("the directory alone reads as here, wherever it stands in the line", () => {
67 const dir = "/data/crew/bot/1/2";
68 expect(stripForgeDirs([dir], `cd ${dir} && ls ${dir}/src`)).toBe(
69 "cd . && ls src",
70 );
71 expect(stripForgeDirs([dir], `at "${dir}/a.ts":3 (${dir})`)).toBe(
72 'at "a.ts":3 (.)',
73 );
74 });
75
76 test("its text inside a longer path is left whole, never a path that was not there", () => {
77 const dir = "/var/folders/x/work";
78 // The same directory through a symlink the forge did not resolve.
79 expect(stripForgeDirs([dir], `/private${dir}/CREW.md`)).toBe(
80 `/private${dir}/CREW.md`,
81 );
82 // A sibling whose name begins the same way.
83 expect(stripForgeDirs(["/data/1/2"], "/data/1/23/a.ts /data/1/2.bak")).toBe(
84 "/data/1/23/a.ts /data/1/2.bak",
85 );
86 // And the whole one beside them still goes.
87 expect(
88 stripForgeDirs(["/data/1/2"], "/data/1/23/a.ts /data/1/2/a.ts"),
89 ).toBe("/data/1/23/a.ts a.ts");
90 });
91
92 test("a directory before a full stop that punctuates goes, and the stop stays", () => {
93 const dir = "/var/folders/x/work";
94 expect(stripForgeDirs([dir], `wrote to ${dir}.`)).toBe("wrote to .");
95 expect(stripForgeDirs([dir], `${dir}. Then ${dir}/a.ts`)).toBe(
96 ". Then a.ts",
97 );
98 // Inside brackets or quotes, before a comma, and in an ellipsis.
99 expect(stripForgeDirs([dir], `(see ${dir}.)`)).toBe("(see .)");
100 expect(stripForgeDirs([dir], `"${dir}.", it said`)).toBe('".", it said');
101 expect(stripForgeDirs([dir], `${dir}...`)).toBe("...");
102 // A name that goes on after the dot is a sibling, left whole — after
103 // one dot or two.
104 expect(stripForgeDirs([dir], `${dir}.bak is kept`)).toBe(
105 `${dir}.bak is kept`,
106 );
107 expect(stripForgeDirs([dir], `${dir}..bak is kept`)).toBe(
108 `${dir}..bak is kept`,
109 );
110 });
111
112 test("a directory reached through a symlink reads relative under either spelling", async () => {
113 const root = realpathSync(await mkdtemp(join(tmpdir(), "forge-paths-")));
114 try {
115 const real = join(root, "real");
116 const link = join(root, "link");
117 mkdirSync(real);
118 symlinkSync(real, link);
119 // The forge named the link; a child process reports the real path.
120 const dirs = spellings([link]);
121 expect(dirs).toEqual([link, real].sort((a, b) => b.length - a.length));
122 expect(stripForgeDirs(dirs, `${real}/CREW.md`)).toBe("CREW.md");
123 expect(stripForgeDirs(dirs, `${link}/CREW.md`)).toBe("CREW.md");
124 // A directory that is not there yet keeps its own spelling.
125 expect(spellings([join(root, "later"), ""])).toEqual([
126 join(root, "later"),
127 ]);
128 } finally {
129 await rm(root, { recursive: true, force: true });
130 }
131 });
132 });
133
13435 describe("the crew, with a stand-in Claude Code", () => {
13536 let standIn: StandInCrew;
13637 let dataDir: string;
@@ -148,7 +49,8 @@ describe("the crew, with a stand-in Claude Code", () => {
14849
14950 beforeAll(async () => {
15051 standIn = await startStandInCrew();
151 ({ dataDir,app,servers,crew,alice,bot,repo,argsFile,promptFile } = standIn);
52 ({ dataDir, app, servers, crew, alice, bot, repo, argsFile, promptFile } =
53 standIn);
15254 }, 30_000);
15355
15456 afterAll(() => stopStandInCrew(standIn));
@@ -284,88 +186,4 @@ describe("the crew, with a stand-in Claude Code", () => {
284186 ),
285187 ).toBe(true);
286188 }, 60_000);
287
288 test("feedback from a person resumes the session on the same branch", async () => {
289 const pull = listPulls(app.db, repo.id, { state: "open" })[0]!;
290 const before = listRuns(app.db, repo.id, { kind: "agent" }).length;
291 // The agent's own comment is not feedback.
292 expect(addComment(app, bot, repo, pull, "Working on it.").ok).toBe(true);
293 await Bun.sleep(300);
294 expect(listRuns(app.db, repo.id, { kind: "agent" }).length).toBe(before);
295 // A person's is.
296 expect(
297 addComment(app, alice, repo, pull, "Also plant daffodils please.").ok,
298 ).toBe(true);
299 const run = await settled((r) => r.reason === "feedback");
300 expect(run.state).toBe("ok");
301 expect(run.summary).toMatch(/^updated pull #\d+/);
302 const args = readFileSync(argsFile, "utf8");
303 expect(args).toContain("--resume\nfake-1");
304 const prompt = readFileSync(promptFile, "utf8");
305 expect(prompt).toContain("feedback on your pull");
306 expect(prompt).toContain("Also plant daffodils please.");
307 expect(prompt).not.toContain("Working on it.");
308 // The agent reported on the pull, and the branch moved.
309 const comments = listComments(app.db, pull.id).filter(
310 (c) => c.authorName === "bot",
311 );
312 expect(comments.map((c) => c.body)).toContain("Added CREW.md as asked.");
313 // The branch moved in the fork (the pull row keeps its opening snapshot;
314 // the live head is git's), and the base's snapshot of it followed.
315 const fork = getForkOf(app.db, bot.id, repo.id)!;
316 const tip = await resolveOid(
317 repoDir(app.reposRoot, fork.ownerName, fork.name),
318 `refs/heads/${pull.headBranch}`,
319 );
320 expect(tip).toBeDefined();
321 expect(tip).not.toBe(pull.headOid);
322 await Bun.sleep(300);
323 expect(
324 await resolveOid(
325 repoDir(app.reposRoot, repo.ownerName, repo.name),
326 `refs/pull/${pull.number}/head`,
327 ),
328 ).toBe(tip);
329 }, 60_000);
330
331 test("closing the pull removes the clone; a closed issue starts nothing", async () => {
332 const pull = listPulls(app.db, repo.id, { state: "open" })[0]!;
333 const dir = workdirFor(
334 dataDir,
335 getAgentByName(app.db, "bot")!,
336 repo,
337 issueNumber,
338 );
339 expect(existsSync(dir)).toBe(true);
340 expect(setIssueState(app, alice, repo, pull, "closed").ok).toBe(true);
341 const deadline = Date.now() + 5_000;
342 while (existsSync(dir) && Date.now() < deadline) await Bun.sleep(50);
343 expect(existsSync(dir)).toBe(false);
344 const before = listRuns(app.db, repo.id, { kind: "agent" }).length;
345 expect(addComment(app, alice, repo, pull, "one more thing").ok).toBe(true);
346 await Bun.sleep(300);
347 expect(listRuns(app.db, repo.id, { kind: "agent" }).length).toBe(before);
348 }, 30_000);
349
350 test("a pull closed without a merge has nothing to continue: its follow-up fails before it begins", async () => {
351 const agent = getAgentByName(app.db, "bot")!;
352 // Closed without a merge, the proposal was declined: nothing to continue.
353 const closed = listPulls(app.db, repo.id, { state: "closed" }).find(
354 (p) => p.mergedAt === null,
355 )!;
356 expect(closed).toBeDefined();
357 const dead = queueAgentRun(
358 app,
359 repo,
360 getIssue(app.db, repo.id, closed.number)!,
361 agent,
362 "feedback",
363 alice.id,
364 )!;
365 const outcome = await settled((r) => r.id === dead.id);
366 expect(outcome.state).toBe("failed");
367 expect(outcome.summary).toBe(
368 `pull #${closed.number} was closed before the run began`,
369 );
370 }, 30_000);
371189 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs