1+ import { execFile } from "node:child_process" ;
12import { mkdir , readFile , statfs , writeFile } from "node:fs/promises" ;
23import { tmpdir } from "node:os" ;
34import { join } from "node:path" ;
5+ import { promisify } from "node:util" ;
46import { getDefaultRunner , getRunner } from "../runners/registry.js" ;
57import { 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" ;
79import type { AgentResult , EnsembleResult , RunOptions } from "../types.js" ;
810import { displayApplyInstructions , displayHeader , displayResults } from "../utils/display.js" ;
911import {
@@ -14,6 +16,8 @@ import {
1416 removeWorktree ,
1517} from "../utils/git.js" ;
1618
19+ const execFileAsync = promisify ( execFile ) ;
20+
1721function 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+
287328export 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