diff --git a/src/services/tools/bash.test.ts b/src/services/tools/bash.test.ts index 8117e9933..3ed6da140 100644 --- a/src/services/tools/bash.test.ts +++ b/src/services/tools/bash.test.ts @@ -268,7 +268,7 @@ describe("bash tool", () => { using testEnv = createTestBashTool(); const tool = testEnv.tool; const args: BashToolArgs = { - script: "sleep 10", + script: "while true; do sleep 0.1; done", timeout_secs: 1, }; @@ -507,7 +507,7 @@ describe("bash tool", () => { const args: BashToolArgs = { // Background process that would block if we waited for it - script: "sleep 100 > /dev/null 2>&1 &", + script: "while true; do sleep 1; done > /dev/null 2>&1 &", timeout_secs: 5, }; @@ -515,7 +515,7 @@ describe("bash tool", () => { const duration = performance.now() - startTime; expect(result.success).toBe(true); - // Should complete in well under 1 second, not wait for sleep 100 + // Should complete in well under 1 second, not wait for infinite loop expect(duration).toBeLessThan(2000); }); @@ -527,7 +527,7 @@ describe("bash tool", () => { const args: BashToolArgs = { // Spawn background process, echo its PID, then exit // Should not wait for the background process - script: "sleep 100 > /dev/null 2>&1 & echo $!", + script: "while true; do sleep 1; done > /dev/null 2>&1 & echo $!", timeout_secs: 5, }; @@ -550,7 +550,7 @@ describe("bash tool", () => { const args: BashToolArgs = { // Background process with output redirected but still blocking - script: "sleep 10 & wait", + script: "while true; do sleep 0.1; done & wait", timeout_secs: 1, }; @@ -655,6 +655,44 @@ describe("bash tool", () => { } }); + it("should block sleep command at start of script", async () => { + using testEnv = createTestBashTool(); + const tool = testEnv.tool; + const args: BashToolArgs = { + script: "sleep 5", + timeout_secs: 10, + }; + + const result = (await tool.execute!(args, mockToolCallOptions)) as BashToolResult; + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toContain("sleep commands are blocked"); + expect(result.error).toContain("polling loops"); + expect(result.error).toContain("while ! condition"); + expect(result.exitCode).toBe(-1); + expect(result.wall_duration_ms).toBe(0); + } + }); + + it("should allow sleep in polling loops", async () => { + using testEnv = createTestBashTool(); + const tool = testEnv.tool; + const args: BashToolArgs = { + script: "for i in 1 2 3; do echo $i; sleep 0.1; done", + timeout_secs: 5, + }; + + const result = (await tool.execute!(args, mockToolCallOptions)) as BashToolResult; + + expect(result.success).toBe(true); + if (result.success) { + expect(result.output).toContain("1"); + expect(result.output).toContain("2"); + expect(result.output).toContain("3"); + } + }); + it("should use default timeout (3s) when timeout_secs is undefined", async () => { using testEnv = createTestBashTool(); const tool = testEnv.tool; diff --git a/src/services/tools/bash.ts b/src/services/tools/bash.ts index 9e2ea7bcf..abd0d4662 100644 --- a/src/services/tools/bash.ts +++ b/src/services/tools/bash.ts @@ -52,6 +52,17 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => { }; } + // Block sleep at the beginning of commands - they waste time waiting. Use polling loops instead. + if (/^\s*sleep\s/.test(script)) { + return { + success: false, + error: + "sleep commands are blocked to minimize waiting time. Instead, use polling loops to check conditions repeatedly (e.g., 'while ! condition; do sleep 1; done' or 'until condition; do sleep 1; done').", + exitCode: -1, + wall_duration_ms: 0, + }; + } + // Default timeout to 3 seconds for interactivity // OpenAI models often don't provide timeout_secs even when marked required, // so we make it optional with a sensible default. diff --git a/tests/ipcMain/executeBash.test.ts b/tests/ipcMain/executeBash.test.ts index 60efdd804..a0eeedcee 100644 --- a/tests/ipcMain/executeBash.test.ts +++ b/tests/ipcMain/executeBash.test.ts @@ -143,7 +143,7 @@ describeIntegration("IpcMain executeBash integration tests", () => { const timeoutResult = await env.mockIpcRenderer.invoke( IPC_CHANNELS.WORKSPACE_EXECUTE_BASH, workspaceId, - "sleep 10", + "while true; do sleep 0.1; done", { timeout_secs: 1 } ); diff --git a/tests/ipcMain/renameWorkspace.test.ts b/tests/ipcMain/renameWorkspace.test.ts index 4b978f009..12cd800d1 100644 --- a/tests/ipcMain/renameWorkspace.test.ts +++ b/tests/ipcMain/renameWorkspace.test.ts @@ -247,7 +247,7 @@ describeIntegration("IpcMain rename workspace integration tests", () => { void sendMessageWithModel( env.mockIpcRenderer, workspaceId, - "Run this bash command: sleep 30 && echo done" + "Run this bash command: for i in {1..60}; do sleep 0.5; done && echo done" ); // Wait for stream to start diff --git a/tests/ipcMain/resumeStream.test.ts b/tests/ipcMain/resumeStream.test.ts index fc0150a07..56e99101f 100644 --- a/tests/ipcMain/resumeStream.test.ts +++ b/tests/ipcMain/resumeStream.test.ts @@ -35,7 +35,7 @@ describeIntegration("IpcMain resumeStream integration tests", () => { void sendMessageWithModel( env.mockIpcRenderer, workspaceId, - `Run this bash command: sleep 5 && echo '${expectedWord}'`, + `Run this bash command: for i in 1 2 3; do sleep 0.5; done && echo '${expectedWord}'`, "anthropic", "claude-sonnet-4-5" ); diff --git a/tests/ipcMain/sendMessage.test.ts b/tests/ipcMain/sendMessage.test.ts index 8e9a56580..335d3c7a7 100644 --- a/tests/ipcMain/sendMessage.test.ts +++ b/tests/ipcMain/sendMessage.test.ts @@ -88,7 +88,7 @@ describeIntegration("IpcMain sendMessage integration tests", () => { const { env, workspaceId, cleanup } = await setupWorkspace(provider); try { // Start a long-running stream with a bash command that takes time - const longMessage = "Run this bash command: sleep 60 && echo done"; + const longMessage = "Run this bash command: while true; do sleep 1; done"; void sendMessageWithModel(env.mockIpcRenderer, workspaceId, longMessage, provider, model); // Wait for stream to start @@ -263,11 +263,11 @@ describeIntegration("IpcMain sendMessage integration tests", () => { const { env, workspaceId, cleanup } = await setupWorkspace(provider); try { - // Start a stream with tool call that takes 10 seconds + // Start a stream with tool call that takes a long time void sendMessageWithModel( env.mockIpcRenderer, workspaceId, - "Run this bash command: sleep 10", + "Run this bash command: while true; do sleep 0.1; done", provider, model ); @@ -279,7 +279,7 @@ describeIntegration("IpcMain sendMessage integration tests", () => { await collector1.waitForEvent("tool-call-start", 10000); - // At this point, bash sleep is running (will take 10 seconds if abort doesn't work) + // At this point, bash loop is running (will run forever if abort doesn't work) // Get message ID for verification collector1.collect(); const messageId = @@ -344,8 +344,8 @@ describeIntegration("IpcMain sendMessage integration tests", () => { expect(partialMessages.length).toBe(0); } - // Note: If test completes quickly (~5s), abort signal worked and killed sleep - // If test takes ~10s, abort signal didn't work and sleep ran to completion + // Note: If test completes quickly (~5s), abort signal worked and killed the loop + // If test takes much longer, abort signal didn't work } finally { await cleanup(); } @@ -448,7 +448,7 @@ describeIntegration("IpcMain sendMessage integration tests", () => { const result1 = await sendMessageWithModel( env.mockIpcRenderer, workspaceId, - "Run this bash command: sleep 10 && echo done", + "Run this bash command: for i in {1..20}; do sleep 0.5; done && echo done", provider, model ); @@ -467,7 +467,7 @@ describeIntegration("IpcMain sendMessage integration tests", () => { const result2 = await sendMessageWithModel( env.mockIpcRenderer, workspaceId, - "Run this bash command: sleep 5 && echo second", + "Run this bash command: for i in {1..10}; do sleep 0.5; done && echo second", provider, model, { editMessageId: (firstUserMessage as { id: string }).id } diff --git a/tests/ipcMain/truncate.test.ts b/tests/ipcMain/truncate.test.ts index bb182cf2d..312631c95 100644 --- a/tests/ipcMain/truncate.test.ts +++ b/tests/ipcMain/truncate.test.ts @@ -261,7 +261,7 @@ describeIntegration("IpcMain truncate integration tests", () => { void sendMessageWithModel( env.mockIpcRenderer, workspaceId, - "Run this bash command: sleep 30 && echo done" + "Run this bash command: for i in {1..60}; do sleep 0.5; done && echo done" ); // Wait for stream to start