blob: 8c57a4b812d3a119197ca2acc75dc777759a6182 [file] [log] [blame]
package claudetool
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
)
func TestBashSlowOk(t *testing.T) {
// Test that slow_ok flag is properly handled
t.Run("SlowOk Flag", func(t *testing.T) {
input := json.RawMessage(`{"command":"echo 'slow test'","slow_ok":true}`)
bashTool := (&BashTool{}).Tool()
toolOut := bashTool.Run(context.Background(), input)
if toolOut.Error != nil {
t.Fatalf("Unexpected error: %v", toolOut.Error)
}
result := toolOut.LLMContent
expected := "slow test\n"
if len(result) == 0 || result[0].Text != expected {
t.Errorf("Expected %q, got %q", expected, result[0].Text)
}
})
// Test that slow_ok with background works
t.Run("SlowOk with Background", func(t *testing.T) {
input := json.RawMessage(`{"command":"echo 'slow background test'","slow_ok":true,"background":true}`)
bashTool := (&BashTool{}).Tool()
toolOut := bashTool.Run(context.Background(), input)
if toolOut.Error != nil {
t.Fatalf("Unexpected error: %v", toolOut.Error)
}
result := toolOut.LLMContent
// Should return background result JSON
var bgResult BackgroundResult
resultStr := result[0].Text
if err := json.Unmarshal([]byte(resultStr), &bgResult); err != nil {
t.Fatalf("Failed to unmarshal background result: %v", err)
}
if bgResult.PID <= 0 {
t.Errorf("Invalid PID returned: %d", bgResult.PID)
}
// Clean up
os.Remove(bgResult.StdoutFile)
os.Remove(bgResult.StderrFile)
os.Remove(filepath.Dir(bgResult.StdoutFile))
})
}
func TestBashTool(t *testing.T) {
var bashTool BashTool
tool := bashTool.Tool()
// Test basic functionality
t.Run("Basic Command", func(t *testing.T) {
input := json.RawMessage(`{"command":"echo 'Hello, world!'"}`)
toolOut := tool.Run(context.Background(), input)
if toolOut.Error != nil {
t.Fatalf("Unexpected error: %v", toolOut.Error)
}
result := toolOut.LLMContent
expected := "Hello, world!\n"
if len(result) == 0 || result[0].Text != expected {
t.Errorf("Expected %q, got %q", expected, result[0].Text)
}
})
// Test with arguments
t.Run("Command With Arguments", func(t *testing.T) {
input := json.RawMessage(`{"command":"echo -n foo && echo -n bar"}`)
toolOut := tool.Run(context.Background(), input)
if toolOut.Error != nil {
t.Fatalf("Unexpected error: %v", toolOut.Error)
}
result := toolOut.LLMContent
expected := "foobar"
if len(result) == 0 || result[0].Text != expected {
t.Errorf("Expected %q, got %q", expected, result[0].Text)
}
})
// Test with slow_ok parameter
t.Run("With SlowOK", func(t *testing.T) {
inputObj := struct {
Command string `json:"command"`
SlowOK bool `json:"slow_ok"`
}{
Command: "sleep 0.1 && echo 'Completed'",
SlowOK: true,
}
inputJSON, err := json.Marshal(inputObj)
if err != nil {
t.Fatalf("Failed to marshal input: %v", err)
}
toolOut := tool.Run(context.Background(), inputJSON)
if toolOut.Error != nil {
t.Fatalf("Unexpected error: %v", toolOut.Error)
}
result := toolOut.LLMContent
expected := "Completed\n"
if len(result) == 0 || result[0].Text != expected {
t.Errorf("Expected %q, got %q", expected, result[0].Text)
}
})
// Test command timeout with custom timeout config
t.Run("Command Timeout", func(t *testing.T) {
// Use a custom BashTool with very short timeout
customTimeouts := &Timeouts{
Fast: 100 * time.Millisecond,
Slow: 100 * time.Millisecond,
Background: 100 * time.Millisecond,
}
customBash := &BashTool{
Timeouts: customTimeouts,
}
tool := customBash.Tool()
input := json.RawMessage(`{"command":"sleep 0.5 && echo 'Should not see this'"}`)
toolOut := tool.Run(context.Background(), input)
if toolOut.Error == nil {
t.Errorf("Expected timeout error, got none")
} else if !strings.Contains(toolOut.Error.Error(), "timed out") {
t.Errorf("Expected timeout error, got: %v", toolOut.Error)
}
})
// Test command that fails
t.Run("Failed Command", func(t *testing.T) {
input := json.RawMessage(`{"command":"exit 1"}`)
toolOut := tool.Run(context.Background(), input)
if toolOut.Error == nil {
t.Errorf("Expected error for failed command, got none")
}
})
// Test invalid input
t.Run("Invalid JSON Input", func(t *testing.T) {
input := json.RawMessage(`{"command":123}`) // Invalid JSON (command must be string)
toolOut := tool.Run(context.Background(), input)
if toolOut.Error == nil {
t.Errorf("Expected error for invalid input, got none")
}
})
}
func TestExecuteBash(t *testing.T) {
ctx := context.Background()
// Test successful command
t.Run("Successful Command", func(t *testing.T) {
req := bashInput{
Command: "echo 'Success'",
}
output, err := executeBash(ctx, req, 5*time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
want := "Success\n"
if output != want {
t.Errorf("Expected %q, got %q", want, output)
}
})
// Test SKETCH=1 environment variable is set
t.Run("SKETCH Environment Variable", func(t *testing.T) {
req := bashInput{
Command: "echo $SKETCH",
}
output, err := executeBash(ctx, req, 5*time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
want := "1\n"
if output != want {
t.Errorf("Expected SKETCH=1, got %q", output)
}
})
// Test command with output to stderr
t.Run("Command with stderr", func(t *testing.T) {
req := bashInput{
Command: "echo 'Error message' >&2 && echo 'Success'",
}
output, err := executeBash(ctx, req, 5*time.Second)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
want := "Error message\nSuccess\n"
if output != want {
t.Errorf("Expected %q, got %q", want, output)
}
})
// Test command that fails with stderr
t.Run("Failed Command with stderr", func(t *testing.T) {
req := bashInput{
Command: "echo 'Error message' >&2 && exit 1",
}
_, err := executeBash(ctx, req, 5*time.Second)
if err == nil {
t.Errorf("Expected error for failed command, got none")
} else if !strings.Contains(err.Error(), "Error message") {
t.Errorf("Expected stderr in error message, got: %v", err)
}
})
// Test timeout
t.Run("Command Timeout", func(t *testing.T) {
req := bashInput{
Command: "sleep 1 && echo 'Should not see this'",
}
start := time.Now()
_, err := executeBash(ctx, req, 100*time.Millisecond)
elapsed := time.Since(start)
// Command should time out after ~100ms, not wait for full 1 second
if elapsed >= 1*time.Second {
t.Errorf("Command did not respect timeout, took %v", elapsed)
}
if err == nil {
t.Errorf("Expected timeout error, got none")
} else if !strings.Contains(err.Error(), "timed out") {
t.Errorf("Expected timeout error, got: %v", err)
}
})
}
func TestBackgroundBash(t *testing.T) {
var bashTool BashTool
tool := bashTool.Tool()
// Test basic background execution
t.Run("Basic Background Command", func(t *testing.T) {
inputObj := struct {
Command string `json:"command"`
Background bool `json:"background"`
}{
Command: "echo 'Hello from background' $SKETCH",
Background: true,
}
inputJSON, err := json.Marshal(inputObj)
if err != nil {
t.Fatalf("Failed to marshal input: %v", err)
}
toolOut := tool.Run(context.Background(), inputJSON)
if toolOut.Error != nil {
t.Fatalf("Unexpected error: %v", toolOut.Error)
}
result := toolOut.LLMContent
// Parse the returned JSON
var bgResult BackgroundResult
resultStr := result[0].Text
if err := json.Unmarshal([]byte(resultStr), &bgResult); err != nil {
t.Fatalf("Failed to unmarshal background result: %v", err)
}
// Verify we got a valid PID
if bgResult.PID <= 0 {
t.Errorf("Invalid PID returned: %d", bgResult.PID)
}
// Verify output files exist
if _, err := os.Stat(bgResult.StdoutFile); os.IsNotExist(err) {
t.Errorf("Stdout file doesn't exist: %s", bgResult.StdoutFile)
}
if _, err := os.Stat(bgResult.StderrFile); os.IsNotExist(err) {
t.Errorf("Stderr file doesn't exist: %s", bgResult.StderrFile)
}
// Wait for the command output to be written to file
waitForFile(t, bgResult.StdoutFile)
// Check file contents
stdoutContent, err := os.ReadFile(bgResult.StdoutFile)
if err != nil {
t.Fatalf("Failed to read stdout file: %v", err)
}
expected := "Hello from background 1\n"
if string(stdoutContent) != expected {
t.Errorf("Expected stdout content %q, got %q", expected, string(stdoutContent))
}
// Clean up
os.Remove(bgResult.StdoutFile)
os.Remove(bgResult.StderrFile)
os.Remove(filepath.Dir(bgResult.StdoutFile))
})
// Test background command with stderr output
t.Run("Background Command with stderr", func(t *testing.T) {
inputObj := struct {
Command string `json:"command"`
Background bool `json:"background"`
}{
Command: "echo 'Output to stdout' && echo 'Output to stderr' >&2",
Background: true,
}
inputJSON, err := json.Marshal(inputObj)
if err != nil {
t.Fatalf("Failed to marshal input: %v", err)
}
toolOut := tool.Run(context.Background(), inputJSON)
if toolOut.Error != nil {
t.Fatalf("Unexpected error: %v", toolOut.Error)
}
result := toolOut.LLMContent
// Parse the returned JSON
var bgResult BackgroundResult
resultStr := result[0].Text
if err := json.Unmarshal([]byte(resultStr), &bgResult); err != nil {
t.Fatalf("Failed to unmarshal background result: %v", err)
}
// Wait for the command output to be written to files
waitForFile(t, bgResult.StdoutFile)
waitForFile(t, bgResult.StderrFile)
// Check stdout content
stdoutContent, err := os.ReadFile(bgResult.StdoutFile)
if err != nil {
t.Fatalf("Failed to read stdout file: %v", err)
}
expectedStdout := "Output to stdout\n"
if string(stdoutContent) != expectedStdout {
t.Errorf("Expected stdout content %q, got %q", expectedStdout, string(stdoutContent))
}
// Check stderr content
stderrContent, err := os.ReadFile(bgResult.StderrFile)
if err != nil {
t.Fatalf("Failed to read stderr file: %v", err)
}
expectedStderr := "Output to stderr\n"
if string(stderrContent) != expectedStderr {
t.Errorf("Expected stderr content %q, got %q", expectedStderr, string(stderrContent))
}
// Clean up
os.Remove(bgResult.StdoutFile)
os.Remove(bgResult.StderrFile)
os.Remove(filepath.Dir(bgResult.StdoutFile))
})
// Test background command running without waiting
t.Run("Background Command Running", func(t *testing.T) {
// Create a script that will continue running after we check
inputObj := struct {
Command string `json:"command"`
Background bool `json:"background"`
}{
Command: "echo 'Running in background' && sleep 5",
Background: true,
}
inputJSON, err := json.Marshal(inputObj)
if err != nil {
t.Fatalf("Failed to marshal input: %v", err)
}
// Start the command in the background
toolOut := tool.Run(context.Background(), inputJSON)
if toolOut.Error != nil {
t.Fatalf("Unexpected error: %v", toolOut.Error)
}
result := toolOut.LLMContent
// Parse the returned JSON
var bgResult BackgroundResult
resultStr := result[0].Text
if err := json.Unmarshal([]byte(resultStr), &bgResult); err != nil {
t.Fatalf("Failed to unmarshal background result: %v", err)
}
// Wait for the command output to be written to file
waitForFile(t, bgResult.StdoutFile)
// Check stdout content
stdoutContent, err := os.ReadFile(bgResult.StdoutFile)
if err != nil {
t.Fatalf("Failed to read stdout file: %v", err)
}
expectedStdout := "Running in background\n"
if string(stdoutContent) != expectedStdout {
t.Errorf("Expected stdout content %q, got %q", expectedStdout, string(stdoutContent))
}
// Verify the process is still running
process, _ := os.FindProcess(bgResult.PID)
err = process.Signal(syscall.Signal(0))
if err != nil {
// Process not running, which is unexpected
t.Error("Process is not running")
} else {
// Expected: process should be running
t.Log("Process correctly running in background")
// Kill it for cleanup
process.Kill()
}
// Clean up
os.Remove(bgResult.StdoutFile)
os.Remove(bgResult.StderrFile)
os.Remove(filepath.Dir(bgResult.StdoutFile))
})
}
func TestBashTimeout(t *testing.T) {
// Test default timeout values
t.Run("Default Timeout Values", func(t *testing.T) {
// Test foreground default timeout
foreground := bashInput{
Command: "echo 'test'",
Background: false,
}
fgTimeout := foreground.timeout(nil)
expectedFg := 30 * time.Second
if fgTimeout != expectedFg {
t.Errorf("Expected foreground default timeout to be %v, got %v", expectedFg, fgTimeout)
}
// Test background default timeout
background := bashInput{
Command: "echo 'test'",
Background: true,
}
bgTimeout := background.timeout(nil)
expectedBg := 24 * time.Hour
if bgTimeout != expectedBg {
t.Errorf("Expected background default timeout to be %v, got %v", expectedBg, bgTimeout)
}
// Test slow_ok timeout
slowOk := bashInput{
Command: "echo 'test'",
Background: false,
SlowOK: true,
}
slowTimeout := slowOk.timeout(nil)
expectedSlow := 15 * time.Minute
if slowTimeout != expectedSlow {
t.Errorf("Expected slow_ok timeout to be %v, got %v", expectedSlow, slowTimeout)
}
// Test custom timeout config
customTimeouts := &Timeouts{
Fast: 5 * time.Second,
Slow: 2 * time.Minute,
Background: 1 * time.Hour,
}
customFast := bashInput{
Command: "echo 'test'",
Background: false,
}
customTimeout := customFast.timeout(customTimeouts)
expectedCustom := 5 * time.Second
if customTimeout != expectedCustom {
t.Errorf("Expected custom timeout to be %v, got %v", expectedCustom, customTimeout)
}
})
}
// waitForFile waits for a file to exist and be non-empty or times out
func waitForFile(t *testing.T, filepath string) {
timeout := time.After(5 * time.Second)
tick := time.NewTicker(10 * time.Millisecond)
defer tick.Stop()
for {
select {
case <-timeout:
t.Fatalf("Timed out waiting for file to exist and have contents: %s", filepath)
return
case <-tick.C:
info, err := os.Stat(filepath)
if err == nil && info.Size() > 0 {
return // File exists and has content
}
}
}
}
// waitForProcessDeath waits for a process to no longer exist or times out
func waitForProcessDeath(t *testing.T, pid int) {
timeout := time.After(5 * time.Second)
tick := time.NewTicker(50 * time.Millisecond)
defer tick.Stop()
for {
select {
case <-timeout:
t.Fatalf("Timed out waiting for process %d to exit", pid)
return
case <-tick.C:
process, _ := os.FindProcess(pid)
err := process.Signal(syscall.Signal(0))
if err != nil {
// Process doesn't exist
return
}
}
}
}