Skip to content

Commit c6564ab

Browse files
that-github-userunknownclaude
authored
Add pre-flight test run on main branch before spawning agents (#127)
Runs test command once on current branch before creating worktrees. If tests fail on main, warns user but continues. Catches broken test environments before wasting time on 5 parallel agents. Generated by thinktank Opus (5 agents, ALL pass, Copeland: #2 at +4). Closes #64 Co-authored-by: unknown <that-github-user@github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7ada8e3 commit c6564ab

File tree

2 files changed

+83
-1
lines changed

2 files changed

+83
-1
lines changed

src/commands/run.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
loadLatestResult,
1111
makeResultFilename,
1212
mergeRetryResults,
13+
preflightTestRun,
1314
preflightValidation,
1415
} from "./run.js";
1516

@@ -436,3 +437,34 @@ describe("checkDiskSpace", () => {
436437
}
437438
});
438439
});
440+
441+
describe("preflightTestRun", () => {
442+
it("returns null when test command succeeds", async () => {
443+
const result = await preflightTestRun("node --version", process.cwd());
444+
assert.equal(result, null);
445+
});
446+
447+
it("returns warning when test command fails", async () => {
448+
const result = await preflightTestRun("node --require ./nonexistent-module.js", process.cwd());
449+
assert.ok(result);
450+
assert.ok(result.includes("failed on the current branch"));
451+
assert.ok(result.includes("test environment may already be broken"));
452+
});
453+
454+
it("returns warning with output snippet when test produces output", async () => {
455+
const result = await preflightTestRun("node --require ./nonexistent-module.js", process.cwd());
456+
assert.ok(result);
457+
assert.ok(result.includes("failed on the current branch"));
458+
});
459+
460+
it("returns null for a passing test with output", async () => {
461+
const result = await preflightTestRun("node --version", process.cwd());
462+
assert.equal(result, null);
463+
});
464+
465+
it("returns warning when command is not found", async () => {
466+
const result = await preflightTestRun("nonexistent-command-xyz", process.cwd());
467+
assert.ok(result);
468+
assert.ok(result.includes("failed on the current branch"));
469+
});
470+
});

src/commands/run.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { execFile } from "node:child_process";
12
import { mkdir, readFile, statfs, writeFile } from "node:fs/promises";
23
import { tmpdir } from "node:os";
34
import { join } from "node:path";
5+
import { promisify } from "node:util";
46
import { getDefaultRunner, getRunner } from "../runners/registry.js";
57
import { analyzeConvergence, copelandRecommend, recommend } from "../scoring/convergence.js";
6-
import { runTests, validateTestCommand } from "../scoring/test-runner.js";
8+
import { parseTestCommand, runTests, validateTestCommand } from "../scoring/test-runner.js";
79
import type { AgentResult, EnsembleResult, RunOptions } from "../types.js";
810
import { displayApplyInstructions, displayHeader, displayResults } from "../utils/display.js";
911
import {
@@ -14,6 +16,8 @@ import {
1416
removeWorktree,
1517
} from "../utils/git.js";
1618

19+
const execFileAsync = promisify(execFile);
20+
1721
function formatBytes(bytes: number): string {
1822
if (bytes >= 1024 * 1024 * 1024) {
1923
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
@@ -163,6 +167,15 @@ export async function retry(opts: RunOptions): Promise<void> {
163167
process.exit(1);
164168
}
165169

170+
// Pre-flight test run: catch broken test environments before spawning agents
171+
if (opts.testCmd) {
172+
const repoRoot = await getRepoRoot();
173+
const testWarning = await preflightTestRun(opts.testCmd, repoRoot);
174+
if (testWarning) {
175+
console.warn(` ⚠ ${testWarning}`);
176+
}
177+
}
178+
166179
// Clean up old worktrees
167180
await cleanupBranches().catch(() => {});
168181

@@ -284,6 +297,34 @@ export async function retry(opts: RunOptions): Promise<void> {
284297
process.removeListener("SIGINT", handleSigint);
285298
}
286299

300+
/**
301+
* Run the test command once on the current branch before spawning agents.
302+
* Returns a warning string if the tests fail, or null if they pass.
303+
*/
304+
export async function preflightTestRun(testCmd: string, repoRoot: string): Promise<string | null> {
305+
const { cmd, args } = parseTestCommand(testCmd);
306+
if (!cmd) return null;
307+
308+
try {
309+
await execFileAsync(cmd, args, {
310+
cwd: repoRoot,
311+
timeout: 60_000,
312+
shell: true,
313+
env: { ...process.env, CI: "true" },
314+
});
315+
return null;
316+
} catch (err: unknown) {
317+
const e = err as { stdout?: string; stderr?: string; code?: number | string };
318+
const output = ((e.stdout ?? "") + (e.stderr ?? "")).trim();
319+
const snippet = output.length > 200 ? `${output.slice(0, 200)}...` : output;
320+
return (
321+
`Test command "${testCmd}" failed on the current branch before spawning agents. ` +
322+
"Your test environment may already be broken.\n" +
323+
(snippet ? ` Output: ${snippet}` : "")
324+
);
325+
}
326+
}
327+
287328
export async function run(opts: RunOptions): Promise<void> {
288329
displayHeader(opts.prompt, opts.attempts, opts.model);
289330

@@ -310,6 +351,15 @@ export async function run(opts: RunOptions): Promise<void> {
310351
process.exit(1);
311352
}
312353

354+
// Pre-flight test run: catch broken test environments before spawning agents
355+
if (opts.testCmd) {
356+
const repoRoot = await getRepoRoot();
357+
const testWarning = await preflightTestRun(opts.testCmd, repoRoot);
358+
if (testWarning) {
359+
console.warn(` ⚠ ${testWarning}`);
360+
}
361+
}
362+
313363
// Clean up any leftover worktrees/branches from previous runs
314364
await cleanupBranches().catch(() => {});
315365

0 commit comments

Comments
 (0)