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
48 changes: 43 additions & 5 deletions src/services/tools/bash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -507,15 +507,15 @@ 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,
};

const result = (await tool.execute!(args, mockToolCallOptions)) as BashToolResult;
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);
});

Expand All @@ -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,
};

Expand All @@ -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,
};

Expand Down Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions src/services/tools/bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion tests/ipcMain/executeBash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
);

Expand Down
2 changes: 1 addition & 1 deletion tests/ipcMain/renameWorkspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/ipcMain/resumeStream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
Expand Down
16 changes: 8 additions & 8 deletions tests/ipcMain/sendMessage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
);
Expand All @@ -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 =
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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
);
Expand All @@ -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 }
Expand Down
2 changes: 1 addition & 1 deletion tests/ipcMain/truncate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down