Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/commands/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
loadLatestResult,
makeResultFilename,
mergeRetryResults,
preflightTestRun,
preflightValidation,
} from "./run.js";

Expand Down Expand Up @@ -436,3 +437,34 @@ describe("checkDiskSpace", () => {
}
});
});

describe("preflightTestRun", () => {
it("returns null when test command succeeds", async () => {
const result = await preflightTestRun("node --version", process.cwd());
assert.equal(result, null);
});

it("returns warning when test command fails", async () => {
const result = await preflightTestRun("node --require ./nonexistent-module.js", process.cwd());
assert.ok(result);
assert.ok(result.includes("failed on the current branch"));
assert.ok(result.includes("test environment may already be broken"));
});

it("returns warning with output snippet when test produces output", async () => {
const result = await preflightTestRun("node --require ./nonexistent-module.js", process.cwd());
assert.ok(result);
assert.ok(result.includes("failed on the current branch"));
});

it("returns null for a passing test with output", async () => {
const result = await preflightTestRun("node --version", process.cwd());
assert.equal(result, null);
});

it("returns warning when command is not found", async () => {
const result = await preflightTestRun("nonexistent-command-xyz", process.cwd());
assert.ok(result);
assert.ok(result.includes("failed on the current branch"));
});
});
52 changes: 51 additions & 1 deletion src/commands/run.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { execFile } from "node:child_process";
import { mkdir, readFile, statfs, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { getDefaultRunner, getRunner } from "../runners/registry.js";
import { analyzeConvergence, copelandRecommend, recommend } from "../scoring/convergence.js";
import { runTests, validateTestCommand } from "../scoring/test-runner.js";
import { parseTestCommand, runTests, validateTestCommand } from "../scoring/test-runner.js";
import type { AgentResult, EnsembleResult, RunOptions } from "../types.js";
import { displayApplyInstructions, displayHeader, displayResults } from "../utils/display.js";
import {
Expand All @@ -14,6 +16,8 @@ import {
removeWorktree,
} from "../utils/git.js";

const execFileAsync = promisify(execFile);

function formatBytes(bytes: number): string {
if (bytes >= 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
Expand Down Expand Up @@ -163,6 +167,15 @@ export async function retry(opts: RunOptions): Promise<void> {
process.exit(1);
}

// Pre-flight test run: catch broken test environments before spawning agents
if (opts.testCmd) {
const repoRoot = await getRepoRoot();
const testWarning = await preflightTestRun(opts.testCmd, repoRoot);
if (testWarning) {
console.warn(` ⚠ ${testWarning}`);
}
}

// Clean up old worktrees
await cleanupBranches().catch(() => {});

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

/**
* Run the test command once on the current branch before spawning agents.
* Returns a warning string if the tests fail, or null if they pass.
*/
export async function preflightTestRun(testCmd: string, repoRoot: string): Promise<string | null> {
const { cmd, args } = parseTestCommand(testCmd);
if (!cmd) return null;

try {
await execFileAsync(cmd, args, {
cwd: repoRoot,
timeout: 60_000,
shell: true,
env: { ...process.env, CI: "true" },
});
return null;
} catch (err: unknown) {
const e = err as { stdout?: string; stderr?: string; code?: number | string };
const output = ((e.stdout ?? "") + (e.stderr ?? "")).trim();
const snippet = output.length > 200 ? `${output.slice(0, 200)}...` : output;
return (
`Test command "${testCmd}" failed on the current branch before spawning agents. ` +
"Your test environment may already be broken.\n" +
(snippet ? ` Output: ${snippet}` : "")
);
}
}

export async function run(opts: RunOptions): Promise<void> {
displayHeader(opts.prompt, opts.attempts, opts.model);

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

// Pre-flight test run: catch broken test environments before spawning agents
if (opts.testCmd) {
const repoRoot = await getRepoRoot();
const testWarning = await preflightTestRun(opts.testCmd, repoRoot);
if (testWarning) {
console.warn(` ⚠ ${testWarning}`);
}
}

// Clean up any leftover worktrees/branches from previous runs
await cleanupBranches().catch(() => {});

Expand Down
Loading