diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/ShellExecuteTool.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/ShellExecuteTool.java index cc8d2b7b2..6d6414c2c 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/ShellExecuteTool.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/ShellExecuteTool.java @@ -57,8 +57,11 @@ public String execute( + " with /, ~, or ..(e.g., ., src).", required = false) String workingDirectory, - @ToolParam(name = "timeout", description = "Timeout in seconds (default: 30)") - int timeout) { + @ToolParam( + name = "timeout", + description = "Timeout in seconds (default: 30)", + required = false) + Integer timeout) { String effectiveCommand = command; if (workingDirectory != null && !workingDirectory.isBlank()) { String wd = workingDirectory.strip(); @@ -69,8 +72,9 @@ public String execute( effectiveCommand = "cd '" + wd.replace("'", "'\\''") + "' && " + command; } + int effectiveTimeout = timeout != null && timeout > 0 ? timeout : 30; ExecuteResponse result = - sandbox.execute(runtimeContext, effectiveCommand, timeout > 0 ? timeout : 30); + sandbox.execute(runtimeContext, effectiveCommand, effectiveTimeout); StringBuilder sb = new StringBuilder(); sb.append("Exit code: ").append(result.exitCode()).append("\n"); diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/ShellExecuteToolTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/ShellExecuteToolTest.java new file mode 100644 index 000000000..0569efa16 --- /dev/null +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/ShellExecuteToolTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.harness.agent.tool; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.agentscope.core.agent.RuntimeContext; +import io.agentscope.core.message.ToolUseBlock; +import io.agentscope.core.tool.AgentTool; +import io.agentscope.core.tool.ToolCallParam; +import io.agentscope.core.tool.Toolkit; +import io.agentscope.harness.agent.filesystem.model.ExecuteResponse; +import io.agentscope.harness.agent.filesystem.sandbox.AbstractSandboxFilesystem; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link ShellExecuteTool}. */ +class ShellExecuteToolTest { + + private static final RuntimeContext RT = RuntimeContext.empty(); + + private AbstractSandboxFilesystem sandbox; + private ShellExecuteTool tool; + + @BeforeEach + void setUp() { + sandbox = mock(AbstractSandboxFilesystem.class); + tool = new ShellExecuteTool(sandbox); + } + + @Test + void schema_onlyRequiresCommand() { + Toolkit toolkit = new Toolkit(); + toolkit.registerTool(tool); + + AgentTool registeredTool = toolkit.getTool(ShellExecuteTool.NAME); + + assertEquals(List.of("command"), registeredTool.getParameters().get("required")); + } + + @Test + void execute_omittedOptionalParameters_defaultsTimeoutToThirtySeconds() { + when(sandbox.execute(RT, "pwd", 30)).thenReturn(new ExecuteResponse("", 0, false)); + Toolkit toolkit = new Toolkit(); + toolkit.registerTool(tool); + Map input = Map.of("command", "pwd"); + ToolUseBlock toolUse = + ToolUseBlock.builder() + .id("call-execute") + .name(ShellExecuteTool.NAME) + .input(input) + .content("{\"command\":\"pwd\"}") + .build(); + + toolkit.callTool( + ToolCallParam.builder() + .toolUseBlock(toolUse) + .input(input) + .runtimeContext(RT) + .build()) + .block(Duration.ofSeconds(3)); + + verify(sandbox).execute(RT, "pwd", 30); + } + + @Test + void execute_explicitTimeout_passesTimeoutToSandbox() { + when(sandbox.execute(RT, "pwd", 10)).thenReturn(new ExecuteResponse("", 0, false)); + + tool.execute(RT, "pwd", null, 10); + + verify(sandbox).execute(RT, "pwd", 10); + } + + @Test + void execute_nonPositiveTimeout_defaultsTimeoutToThirtySeconds() { + when(sandbox.execute(RT, "pwd", 30)).thenReturn(new ExecuteResponse("", 0, false)); + + tool.execute(RT, "pwd", null, 0); + + verify(sandbox).execute(RT, "pwd", 30); + } +}