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

test/landing.test.ts drives the crew directly and names the step that did not happen, so it cannot go red on a slow tick #3513

Merged opened this

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

Merged

scout/forge:scout/3493-test-landing-test-ts-drives-the-crew-dir → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/3493-test-landing-test-ts-drives-the-crew-dir was merged into main as 8885c0b.

Review

Sign in to review this change.
3 files changed +37 −12
src/crew/dispatch.ts +1 −1
@@ -216,7 +216,7 @@ export async function startCrew(
216216 );
217217 }
218218 pump(app, ready, inFlight);
219 }, TICK_MS);
219 }, options.tickMs ?? TICK_MS);

Fine. At 50 ms the autonomy/converge sweep stays on its own AUTONOMY_TICK_MS guard, so the fast tick only speeds the pump. A tiny test that tickMs is honored would pin this.

220220 requeueInterrupted(app);
221221 sweepAssigned(app);
222222 pump(app, ready, inFlight);
src/crew/runner.ts +2
@@ -168,6 +168,8 @@ export interface CrewOptions {
168168 readonly sandboxEnforced?: boolean | undefined;
169169 /** Why it cannot, when it cannot — said once in every run's log. */
170170 readonly sandboxNote?: string | undefined;
171 /** Milliseconds between the crew's ticks; tests pass a small one. @default 5000 */
172 readonly tickMs?: number | undefined;
171173 }
172174
173175 export { crewRoot, workdirFor } from "./workdir.ts";
test/landing.test.ts +34 −11
@@ -47,19 +47,39 @@ sleep 60
4747 concurrency: 1,
4848 budgetUsd: 1,
4949 runTimeoutMs: 60_000,
50 tickMs: 50,
5051 });
52 // Where the run stands, for a failure to name: its state and summary,
53 // whether the clone exists, the tail of its log.
54 const seen = (repoId: string, clone?: string): string => {
55 const runs = listRuns(app.db, repoId, { kind: "agent" });
56 const last = runs[0];
57 let tail = "(no run log)";
58 if (last) {
59 try {
60 tail = readFileSync(join(dataDir, "runs", `${last.id}.log`), "utf8")
61 .split("\n")
62 .slice(-20)
63 .join("\n");
64 } catch {
65 // no log yet
66 }
67 }
68 return `runs=${JSON.stringify(runs.map((r) => [r.reason, r.state, r.summary]))} clone=${clone === undefined ? "n/a" : existsSync(clone)}\nlog tail:\n${tail}`;
69 };
5170 const until = async <T>(
5271 read: () => T | undefined,
5372 what: string,
54 ms = 45_000,
73 state: () => string,
74 ms = 20_000,
5575 ): Promise<T> => {
5676 const deadline = Date.now() + ms;
5777 while (Date.now() < deadline) {
5878 const value = read();
5979 if (value !== undefined) return value;
60 await Bun.sleep(50);
80 await Bun.sleep(10);
6181 }
62 throw new Error(`${what}: did not happen in time`);
82 throw new Error(`${what}: did not happen in ${ms} ms; saw ${state()}`);
6383 };
6484 try {
6585 const a = await createUser(app.db, { origin: "seed", trusted: true,
@@ -118,11 +138,14 @@ sleep 60
118138 listRuns(app.db, repo.id, { kind: "agent" }).find(
119139 (r) => r.state === "running",
120140 ),
121 `the run (${JSON.stringify(listRuns(app.db, repo.id, { kind: "agent" }).map((r) => [r.reason, r.state, r.summary]))})`,
141 "the run starting",
142 () => seen(repo.id),
122143 );
123144 expect(run.reason).toBe("feedback");
124145 const clone = workdirFor(dataDir, bot, repo, 1);
125 await until(() => (existsSync(clone) ? true : undefined), "the clone");
146 await until(() => (existsSync(clone) ? true : undefined), "the clone appearing", () =>
147 seen(repo.id, clone),
148 );
126149
127150 // The pull closes under it.
128151 expect(setIssueState(app, alice, repo, asIssue, "closed").ok).toBe(true);
@@ -131,13 +154,14 @@ sleep 60
131154 (x) => x.id === run.id,
132155 );
133156 return r && r.state !== "running" ? r : undefined;
134 }, "the run's end");
157 }, "the run's end", () => seen(repo.id, clone));
135158 expect(ended.state).toBe("cancelled");
136159 expect(ended.summary).toBe("the pull closed");
137160 // The clone went with the run, once the run was over.
138161 await until(
139162 () => (existsSync(clone) ? undefined : true),
140 "the clone's end",
163 "the clone's removal",
164 () => seen(repo.id, clone),
141165 );
142166 const log = readFileSync(join(dataDir, "runs", `${run.id}.log`), "utf8");
143167 expect(log).not.toContain("the run failed");
@@ -154,8 +178,7 @@ sleep 60
154178 app.close();
155179 await rm(dataDir, { recursive: true, force: true });
156180 }
157 // The crew starts a queued run on its next tick, five seconds apart:
158 // the default timeout is a race against that clock, and a loaded check
159 // machine stretches every wait: the budgets are generous on purpose.
160 }, 150_000);
181 // The crew ticks every 50 ms here (`tickMs`), not every five seconds, so
182 // nothing waits on the default clock.
183 }, 30_000);
161184 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs