Skip to content

Commit 4d30513

Browse files
that-github-userunknownclaude
authored
Add --test-timeout flag and fix CI node_modules symlink in diff (#92)
- Configurable test timeout via --test-timeout <seconds> (default 120) - Pass testTimeout through RunOptions → test runner - Fix CI: exclude node_modules symlink from git add in getDiff/getDiffStats (symlink was showing as changed file, breaking worktree lifecycle tests) Generated by thinktank Opus (5 agents, 71% convergence, Agent #2). CI fix applied manually. Closes #58 Co-authored-by: unknown <that-github-user@github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ffeb65c commit 4d30513

7 files changed

Lines changed: 29 additions & 7 deletions

File tree

src/cli.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ program
2424
.option("-n, --attempts <number>", "Number of parallel attempts", "3")
2525
.option("-f, --file <path>", "Read prompt from a file (avoids shell expansion issues)")
2626
.option("-t, --test-cmd <command>", "Test command to verify results (e.g., 'npm test')")
27+
.option("--test-timeout <seconds>", "Timeout for test command in seconds", "120")
2728
.option("--timeout <seconds>", "Timeout per agent in seconds", "300")
2829
.option("--model <model>", "Claude model to use", "sonnet")
2930
.option("-r, --runner <name>", "AI coding tool to use (default: claude-code)")
@@ -37,6 +38,12 @@ program
3738
process.exit(1);
3839
}
3940

41+
const testTimeout = parseInt(opts.testTimeout, 10);
42+
if (Number.isNaN(testTimeout) || testTimeout < 10 || testTimeout > 600) {
43+
console.error("Error: --test-timeout must be a number between 10 and 600 seconds");
44+
process.exit(1);
45+
}
46+
4047
const timeout = parseInt(opts.timeout, 10);
4148
if (Number.isNaN(timeout) || timeout < 10 || timeout > 600) {
4249
console.error("Error: --timeout must be a number between 10 and 600 seconds");
@@ -54,6 +61,7 @@ program
5461
prompt,
5562
attempts,
5663
testCmd: opts.testCmd,
64+
testTimeout,
5765
timeout,
5866
model: opts.model,
5967
runner: opts.runner,

src/commands/run.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ function makeOpts(overrides: Partial<RunOptions> = {}): RunOptions {
77
return {
88
prompt: "fix the bug",
99
attempts: 3,
10+
testTimeout: 120,
1011
timeout: 300,
1112
model: "sonnet",
1213
verbose: false,

src/commands/run.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ export async function run(opts: RunOptions): Promise<void> {
112112

113113
if (opts.testCmd) {
114114
console.log(` Running tests: ${opts.testCmd}`);
115-
const testPromises = worktrees.map(({ id, path }) => runTests(id, opts.testCmd!, path));
115+
const testTimeoutMs = opts.testTimeout * 1000;
116+
const testPromises = worktrees.map(({ id, path }) =>
117+
runTests(id, opts.testCmd!, path, testTimeoutMs),
118+
);
116119
testResults = await Promise.all(testPromises);
117120

118121
for (const test of testResults) {

src/scoring/test-runner.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,12 @@ describe("runTests", () => {
107107
assert.equal(result.passed, false);
108108
assert.ok(result.exitCode !== 0);
109109
});
110+
111+
it("respects custom timeout parameter", async () => {
112+
// Use a very short timeout (100ms) with a command that sleeps longer
113+
const result = await runTests(1, "node -e process.stdin.resume()", ".", 100);
114+
assert.equal(result.passed, false);
115+
assert.equal(result.exitCode, 124);
116+
assert.ok(result.output.includes("timed out after 0.1s"));
117+
});
110118
});

src/scoring/test-runner.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { TestResult } from "../types.js";
66

77
const exec = promisify(execFile);
88

9-
const TEST_TIMEOUT_MS = 120_000;
9+
const DEFAULT_TEST_TIMEOUT_MS = 120_000;
1010

1111
/** Shell operators that indicate command chaining — reject these. */
1212
const SHELL_OPERATORS = /[;|&`><]/;
@@ -52,6 +52,7 @@ export async function runTests(
5252
agentId: number,
5353
testCmd: string,
5454
worktreePath: string,
55+
timeoutMs: number = DEFAULT_TEST_TIMEOUT_MS,
5556
): Promise<TestResult> {
5657
// Security: validate command before execution
5758
const validationError = validateTestCommand(testCmd);
@@ -91,7 +92,7 @@ export async function runTests(
9192
// while keeping args as an array to prevent injection via arguments.
9293
const { stdout, stderr } = await exec(cmd, args, {
9394
cwd: worktreePath,
94-
timeout: TEST_TIMEOUT_MS,
95+
timeout: timeoutMs,
9596
shell: true,
9697
env: { ...process.env, CI: "true" },
9798
});
@@ -114,7 +115,7 @@ export async function runTests(
114115
return {
115116
agentId,
116117
passed: false,
117-
output: `Test command timed out after ${TEST_TIMEOUT_MS / 1000}s`,
118+
output: `Test command timed out after ${timeoutMs / 1000}s`,
118119
exitCode: 124,
119120
};
120121
}

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export interface RunOptions {
22
prompt: string;
33
attempts: number;
44
testCmd?: string;
5+
testTimeout: number;
56
timeout: number;
67
model: string;
78
verbose: boolean;

src/utils/git.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ export async function removeWorktree(worktreePath: string): Promise<void> {
7777
export async function getDiff(worktreePath: string): Promise<string> {
7878
try {
7979
// Include both staged and unstaged changes relative to HEAD
80-
// First add all changes so they show in the diff
81-
await exec("git", ["add", "-A"], { cwd: worktreePath });
80+
// Exclude node_modules symlink (created by createWorktree for tool access)
81+
await exec("git", ["add", "-A", "--", ".", ":!node_modules"], { cwd: worktreePath });
8282
const { stdout } = await exec("git", ["diff", "--cached", "HEAD"], {
8383
cwd: worktreePath,
8484
});
@@ -95,7 +95,7 @@ export async function getDiffStats(
9595
worktreePath: string,
9696
): Promise<{ filesChanged: string[]; linesAdded: number; linesRemoved: number }> {
9797
try {
98-
await exec("git", ["add", "-A"], { cwd: worktreePath });
98+
await exec("git", ["add", "-A", "--", ".", ":!node_modules"], { cwd: worktreePath });
9999
const { stdout } = await exec("git", ["diff", "--cached", "--stat", "HEAD"], {
100100
cwd: worktreePath,
101101
});

0 commit comments

Comments
 (0)