diff --git a/core/tools/implementations/runTerminalCommand.ts b/core/tools/implementations/runTerminalCommand.ts index 8f6201bd12c..d9e8df01cb5 100644 --- a/core/tools/implementations/runTerminalCommand.ts +++ b/core/tools/implementations/runTerminalCommand.ts @@ -27,7 +27,15 @@ function getShellCommand(command: string): { shell: string; args: string[] } { // Windows: Use PowerShell return { shell: "powershell.exe", - args: ["-NoLogo", "-ExecutionPolicy", "Bypass", "-Command", command], + args: [ + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-Command", + command, + ], }; } else { // Unix/macOS: Use login shell to source .bashrc/.zshrc etc. @@ -150,6 +158,7 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { cwd, env: getColorEnv(), // Add enhanced environment for colors }); + childProc.stdin?.end(); // Track this process for foreground cancellation if (toolCallId && waitForCompletion) { diff --git a/extensions/cli/src/tools/runTerminalCommand.test.ts b/extensions/cli/src/tools/runTerminalCommand.test.ts index 32e8ba8ecb2..5633a73720d 100644 --- a/extensions/cli/src/tools/runTerminalCommand.test.ts +++ b/extensions/cli/src/tools/runTerminalCommand.test.ts @@ -77,6 +77,14 @@ describe("runTerminalCommandTool", () => { /Error \(exit code|Command timed out|not found|not recognized/, ); }); + + it("should reject on non-zero exit code even with empty stderr", async () => { + const command = "exit 42"; + + await expect(runTerminalCommandTool.run({ command })).rejects.toMatch( + /Error \(exit code 42\)/, + ); + }); }); describe("platform-specific features", () => { diff --git a/extensions/cli/src/tools/runTerminalCommand.ts b/extensions/cli/src/tools/runTerminalCommand.ts index 6b640c6a1c3..e4984c9bcdb 100644 --- a/extensions/cli/src/tools/runTerminalCommand.ts +++ b/extensions/cli/src/tools/runTerminalCommand.ts @@ -68,7 +68,15 @@ function getShellCommand(command: string): { shell: string; args: string[] } { // Windows: Use PowerShell return { shell: "powershell.exe", - args: ["-NoLogo", "-ExecutionPolicy", "Bypass", "-Command", command], + args: [ + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-Command", + command, + ], }; } @@ -190,12 +198,14 @@ IMPORTANT: To edit files, use Edit/MultiEdit tools instead of bash commands (sed // Use same shell logic as core implementation const { shell, args } = getShellCommand(command); const child = spawn(shell, args); + // Close stdin immediately as this tool runs non-interactively and should not wait on stdin + child.stdin?.end(); let stdout = ""; let stderr = ""; let timeoutId: NodeJS.Timeout; let isResolved = false; - // Determine timeout: use provided timeout (capped at 600s), test env variable, or default 120s + // Determine timeout: use provided timeout (capped at 600s), test env variable, or default 180s let TIMEOUT_MS = 180000; // 180 seconds default if (timeout !== undefined) { // Cap at 600 seconds (10 minutes) @@ -206,6 +216,8 @@ IMPORTANT: To edit files, use Edit/MultiEdit tools instead of bash commands (sed process.env.TEST_TERMINAL_TIMEOUT ) { TIMEOUT_MS = parseInt(process.env.TEST_TERMINAL_TIMEOUT, 10); + } else if (process.env.NODE_ENV === "test") { + TIMEOUT_MS = 15000; } /** @@ -330,9 +342,10 @@ IMPORTANT: To edit files, use Edit/MultiEdit tools instead of bash commands (sed moveToBackground, ); - // Only reject on non-zero exit code if there's also stderr - if (code !== 0 && stderr) { - reject(`Error (exit code ${code}): ${stderr}`); + // Reject on non-zero exit code + if (code !== 0 && code !== null) { + const errorMessage = stderr || stdout || "Command failed"; + reject(`Error (exit code ${code}): ${errorMessage}`); return; } diff --git a/extensions/cli/vitest.setup.ts b/extensions/cli/vitest.setup.ts index efbec2ab8db..734b01ab7d3 100644 --- a/extensions/cli/vitest.setup.ts +++ b/extensions/cli/vitest.setup.ts @@ -6,6 +6,9 @@ import { resetConsoleOverrides } from "./src/init.js"; process.env.CONTINUE_CLI_ENABLE_TELEMETRY = "0"; process.env.CONTINUE_ALLOW_ANONYMOUS_TELEMETRY = "0"; +// Ensure terminal command timeout in tests fires before vitest's 30s test timeout +process.env.TEST_TERMINAL_TIMEOUT = "15000"; + // Mock fetch to prevent actual API calls in tests const originalFetch = global.fetch; global.fetch = vi