blob: 45dd4fe58d457f2f80e7574277da6c68824572d1 [file] [log] [blame]
package claudetool
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
)
func TestBashRun(t *testing.T) {
// Test basic functionality
t.Run("Basic Command", func(t *testing.T) {
input := json.RawMessage(`{"command":"echo 'Hello, world!'"}`)
result, err := BashRun(context.Background(), input)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
expected := "Hello, world!\n"
resultStr := ContentToString(result)
if resultStr != expected {
t.Errorf("Expected %q, got %q", expected, resultStr)
}
})
// Test with arguments
t.Run("Command With Arguments", func(t *testing.T) {
input := json.RawMessage(`{"command":"echo -n foo && echo -n bar"}`)
result, err := BashRun(context.Background(), input)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
expected := "foobar"
resultStr := ContentToString(result)
if resultStr != expected {
t.Errorf("Expected %q, got %q", expected, resultStr)
}
})
// Test with timeout parameter
t.Run("With Timeout", func(t *testing.T) {
inputObj := struct {
Command string `json:"command"`
Timeout string `json:"timeout"`
}{
Command: "sleep 0.1 && echo 'Completed'",
Timeout: "5s",
}
inputJSON, err := json.Marshal(inputObj)
if err != nil {
t.Fatalf("Failed to marshal input: %v", err)
}
result, err := BashRun(context.Background(), inputJSON)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
expected := "Completed\n"
resultStr := ContentToString(result)
if resultStr != expected {
t.Errorf("Expected %q, got %q", expected, resultStr)
}
})
// Test command timeout
t.Run("Command Timeout", func(t *testing.T) {
inputObj := struct {
Command string `json:"command"`
Timeout string `json:"timeout"`
}{
Command: "sleep 0.5 && echo 'Should not see this'",
Timeout: "100ms",
}
inputJSON, err := json.Marshal(inputObj)
if err != nil {
t.Fatalf("Failed to marshal input: %v", err)
}
_, err = BashRun(context.Background(), inputJSON)
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)
}
})
// Test command that fails
t.Run("Failed Command", func(t *testing.T) {
input := json.RawMessage(`{"command":"exit 1"}`)
_, err := BashRun(context.Background(), input)
if err == 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)
_, err := BashRun(context.Background(), input)
if err == 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'",
Timeout: "5s",
}
output, err := executeBash(ctx, req)
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",
Timeout: "5s",
}
output, err := executeBash(ctx, req)
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'",
Timeout: "5s",
}
output, err := executeBash(ctx, req)
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",
Timeout: "5s",
}
_, err := executeBash(ctx, req)
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'",
Timeout: "100ms",
}
start := time.Now()
_, err := executeBash(ctx, req)
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) {
// 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)
}
result, err := BashRun(context.Background(), inputJSON)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
// Parse the returned JSON
var bgResult BackgroundResult
resultStr := ContentToString(result)
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)
}
result, err := BashRun(context.Background(), inputJSON)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
// Parse the returned JSON
var bgResult BackgroundResult
resultStr := ContentToString(result)
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
result, err := BashRun(context.Background(), inputJSON)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
// Parse the returned JSON
var bgResult BackgroundResult
resultStr := ContentToString(result)
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()
expectedFg := 10 * 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()
expectedBg := 10 * time.Minute
if bgTimeout != expectedBg {
t.Errorf("Expected background default timeout to be %v, got %v", expectedBg, bgTimeout)
}
// Test explicit timeout overrides defaults
explicit := bashInput{
Command: "echo 'test'",
Background: true,
Timeout: "5s",
}
explicitTimeout := explicit.timeout()
expectedExplicit := 5 * time.Second
if explicitTimeout != expectedExplicit {
t.Errorf("Expected explicit timeout to be %v, got %v", expectedExplicit, explicitTimeout)
}
})
}
// 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
}
}
}
}