Skip to content

Commit 832db57

Browse files
unknownclaude
andcommitted
Add pre-flight test run on main branch before spawning agents
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: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7ada8e3 commit 832db57

2 files changed

Lines changed: 83 additions & 1 deletion

File tree

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)