blob: f39114e02e9e897c6302af483e162e4bd3c66900 [file] [log] [blame]
Earl Lee2e463fb2025-04-17 11:22:22 -07001package claudetool
2
3import (
4 "context"
5 "encoding/json"
Philip Zeyligerb60f0f22025-04-23 18:19:32 +00006 "os"
7 "path/filepath"
Earl Lee2e463fb2025-04-17 11:22:22 -07008 "strings"
Philip Zeyligerb60f0f22025-04-23 18:19:32 +00009 "syscall"
Earl Lee2e463fb2025-04-17 11:22:22 -070010 "testing"
11 "time"
12)
13
14func TestBashRun(t *testing.T) {
15 // Test basic functionality
16 t.Run("Basic Command", func(t *testing.T) {
17 input := json.RawMessage(`{"command":"echo 'Hello, world!'"}`)
18
19 result, err := BashRun(context.Background(), input)
20 if err != nil {
21 t.Fatalf("Unexpected error: %v", err)
22 }
23
24 expected := "Hello, world!\n"
25 if result != expected {
26 t.Errorf("Expected %q, got %q", expected, result)
27 }
28 })
29
30 // Test with arguments
31 t.Run("Command With Arguments", func(t *testing.T) {
32 input := json.RawMessage(`{"command":"echo -n foo && echo -n bar"}`)
33
34 result, err := BashRun(context.Background(), input)
35 if err != nil {
36 t.Fatalf("Unexpected error: %v", err)
37 }
38
39 expected := "foobar"
40 if result != expected {
41 t.Errorf("Expected %q, got %q", expected, result)
42 }
43 })
44
45 // Test with timeout parameter
46 t.Run("With Timeout", func(t *testing.T) {
47 inputObj := struct {
48 Command string `json:"command"`
49 Timeout string `json:"timeout"`
50 }{
51 Command: "sleep 0.1 && echo 'Completed'",
52 Timeout: "5s",
53 }
54 inputJSON, err := json.Marshal(inputObj)
55 if err != nil {
56 t.Fatalf("Failed to marshal input: %v", err)
57 }
58
59 result, err := BashRun(context.Background(), inputJSON)
60 if err != nil {
61 t.Fatalf("Unexpected error: %v", err)
62 }
63
64 expected := "Completed\n"
65 if result != expected {
66 t.Errorf("Expected %q, got %q", expected, result)
67 }
68 })
69
70 // Test command timeout
71 t.Run("Command Timeout", func(t *testing.T) {
72 inputObj := struct {
73 Command string `json:"command"`
74 Timeout string `json:"timeout"`
75 }{
76 Command: "sleep 0.5 && echo 'Should not see this'",
77 Timeout: "100ms",
78 }
79 inputJSON, err := json.Marshal(inputObj)
80 if err != nil {
81 t.Fatalf("Failed to marshal input: %v", err)
82 }
83
84 _, err = BashRun(context.Background(), inputJSON)
85 if err == nil {
86 t.Errorf("Expected timeout error, got none")
87 } else if !strings.Contains(err.Error(), "timed out") {
88 t.Errorf("Expected timeout error, got: %v", err)
89 }
90 })
91
92 // Test command that fails
93 t.Run("Failed Command", func(t *testing.T) {
94 input := json.RawMessage(`{"command":"exit 1"}`)
95
96 _, err := BashRun(context.Background(), input)
97 if err == nil {
98 t.Errorf("Expected error for failed command, got none")
99 }
100 })
101
102 // Test invalid input
103 t.Run("Invalid JSON Input", func(t *testing.T) {
104 input := json.RawMessage(`{"command":123}`) // Invalid JSON (command must be string)
105
106 _, err := BashRun(context.Background(), input)
107 if err == nil {
108 t.Errorf("Expected error for invalid input, got none")
109 }
110 })
111}
112
113func TestExecuteBash(t *testing.T) {
114 ctx := context.Background()
115
116 // Test successful command
117 t.Run("Successful Command", func(t *testing.T) {
118 req := bashInput{
119 Command: "echo 'Success'",
120 Timeout: "5s",
121 }
122
123 output, err := executeBash(ctx, req)
124 if err != nil {
125 t.Fatalf("Unexpected error: %v", err)
126 }
127
128 want := "Success\n"
129 if output != want {
130 t.Errorf("Expected %q, got %q", want, output)
131 }
132 })
133
134 // Test command with output to stderr
135 t.Run("Command with stderr", func(t *testing.T) {
136 req := bashInput{
137 Command: "echo 'Error message' >&2 && echo 'Success'",
138 Timeout: "5s",
139 }
140
141 output, err := executeBash(ctx, req)
142 if err != nil {
143 t.Fatalf("Unexpected error: %v", err)
144 }
145
146 want := "Error message\nSuccess\n"
147 if output != want {
148 t.Errorf("Expected %q, got %q", want, output)
149 }
150 })
151
152 // Test command that fails with stderr
153 t.Run("Failed Command with stderr", func(t *testing.T) {
154 req := bashInput{
155 Command: "echo 'Error message' >&2 && exit 1",
156 Timeout: "5s",
157 }
158
159 _, err := executeBash(ctx, req)
160 if err == nil {
161 t.Errorf("Expected error for failed command, got none")
162 } else if !strings.Contains(err.Error(), "Error message") {
163 t.Errorf("Expected stderr in error message, got: %v", err)
164 }
165 })
166
167 // Test timeout
168 t.Run("Command Timeout", func(t *testing.T) {
169 req := bashInput{
170 Command: "sleep 1 && echo 'Should not see this'",
171 Timeout: "100ms",
172 }
173
174 start := time.Now()
175 _, err := executeBash(ctx, req)
176 elapsed := time.Since(start)
177
178 // Command should time out after ~100ms, not wait for full 1 second
179 if elapsed >= 1*time.Second {
180 t.Errorf("Command did not respect timeout, took %v", elapsed)
181 }
182
183 if err == nil {
184 t.Errorf("Expected timeout error, got none")
185 } else if !strings.Contains(err.Error(), "timed out") {
186 t.Errorf("Expected timeout error, got: %v", err)
187 }
188 })
189}
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000190
191func TestBackgroundBash(t *testing.T) {
192 // Test basic background execution
193 t.Run("Basic Background Command", func(t *testing.T) {
194 inputObj := struct {
195 Command string `json:"command"`
196 Background bool `json:"background"`
197 }{
198 Command: "echo 'Hello from background'",
199 Background: true,
200 }
201 inputJSON, err := json.Marshal(inputObj)
202 if err != nil {
203 t.Fatalf("Failed to marshal input: %v", err)
204 }
205
206 result, err := BashRun(context.Background(), inputJSON)
207 if err != nil {
208 t.Fatalf("Unexpected error: %v", err)
209 }
210
211 // Parse the returned JSON
212 var bgResult BackgroundResult
213 if err := json.Unmarshal([]byte(result), &bgResult); err != nil {
214 t.Fatalf("Failed to unmarshal background result: %v", err)
215 }
216
217 // Verify we got a valid PID
218 if bgResult.PID <= 0 {
219 t.Errorf("Invalid PID returned: %d", bgResult.PID)
220 }
221
222 // Verify output files exist
223 if _, err := os.Stat(bgResult.StdoutFile); os.IsNotExist(err) {
224 t.Errorf("Stdout file doesn't exist: %s", bgResult.StdoutFile)
225 }
226 if _, err := os.Stat(bgResult.StderrFile); os.IsNotExist(err) {
227 t.Errorf("Stderr file doesn't exist: %s", bgResult.StderrFile)
228 }
229
230 // Wait for the command output to be written to file
231 waitForFile(t, bgResult.StdoutFile)
232
233 // Check file contents
234 stdoutContent, err := os.ReadFile(bgResult.StdoutFile)
235 if err != nil {
236 t.Fatalf("Failed to read stdout file: %v", err)
237 }
238 expected := "Hello from background\n"
239 if string(stdoutContent) != expected {
240 t.Errorf("Expected stdout content %q, got %q", expected, string(stdoutContent))
241 }
242
243 // Clean up
244 os.Remove(bgResult.StdoutFile)
245 os.Remove(bgResult.StderrFile)
246 os.Remove(filepath.Dir(bgResult.StdoutFile))
247 })
248
249 // Test background command with stderr output
250 t.Run("Background Command with stderr", func(t *testing.T) {
251 inputObj := struct {
252 Command string `json:"command"`
253 Background bool `json:"background"`
254 }{
255 Command: "echo 'Output to stdout' && echo 'Output to stderr' >&2",
256 Background: true,
257 }
258 inputJSON, err := json.Marshal(inputObj)
259 if err != nil {
260 t.Fatalf("Failed to marshal input: %v", err)
261 }
262
263 result, err := BashRun(context.Background(), inputJSON)
264 if err != nil {
265 t.Fatalf("Unexpected error: %v", err)
266 }
267
268 // Parse the returned JSON
269 var bgResult BackgroundResult
270 if err := json.Unmarshal([]byte(result), &bgResult); err != nil {
271 t.Fatalf("Failed to unmarshal background result: %v", err)
272 }
273
274 // Wait for the command output to be written to files
275 waitForFile(t, bgResult.StdoutFile)
276 waitForFile(t, bgResult.StderrFile)
277
278 // Check stdout content
279 stdoutContent, err := os.ReadFile(bgResult.StdoutFile)
280 if err != nil {
281 t.Fatalf("Failed to read stdout file: %v", err)
282 }
283 expectedStdout := "Output to stdout\n"
284 if string(stdoutContent) != expectedStdout {
285 t.Errorf("Expected stdout content %q, got %q", expectedStdout, string(stdoutContent))
286 }
287
288 // Check stderr content
289 stderrContent, err := os.ReadFile(bgResult.StderrFile)
290 if err != nil {
291 t.Fatalf("Failed to read stderr file: %v", err)
292 }
293 expectedStderr := "Output to stderr\n"
294 if string(stderrContent) != expectedStderr {
295 t.Errorf("Expected stderr content %q, got %q", expectedStderr, string(stderrContent))
296 }
297
298 // Clean up
299 os.Remove(bgResult.StdoutFile)
300 os.Remove(bgResult.StderrFile)
301 os.Remove(filepath.Dir(bgResult.StdoutFile))
302 })
303
304 // Test background command running without waiting
305 t.Run("Background Command Running", func(t *testing.T) {
306 // Create a script that will continue running after we check
307 inputObj := struct {
308 Command string `json:"command"`
309 Background bool `json:"background"`
310 }{
311 Command: "echo 'Running in background' && sleep 5",
312 Background: true,
313 }
314 inputJSON, err := json.Marshal(inputObj)
315 if err != nil {
316 t.Fatalf("Failed to marshal input: %v", err)
317 }
318
319 // Start the command in the background
320 result, err := BashRun(context.Background(), inputJSON)
321 if err != nil {
322 t.Fatalf("Unexpected error: %v", err)
323 }
324
325 // Parse the returned JSON
326 var bgResult BackgroundResult
327 if err := json.Unmarshal([]byte(result), &bgResult); err != nil {
328 t.Fatalf("Failed to unmarshal background result: %v", err)
329 }
330
331 // Wait for the command output to be written to file
332 waitForFile(t, bgResult.StdoutFile)
333
334 // Check stdout content
335 stdoutContent, err := os.ReadFile(bgResult.StdoutFile)
336 if err != nil {
337 t.Fatalf("Failed to read stdout file: %v", err)
338 }
339 expectedStdout := "Running in background\n"
340 if string(stdoutContent) != expectedStdout {
341 t.Errorf("Expected stdout content %q, got %q", expectedStdout, string(stdoutContent))
342 }
343
344 // Verify the process is still running
345 process, _ := os.FindProcess(bgResult.PID)
346 err = process.Signal(syscall.Signal(0))
347 if err != nil {
348 // Process not running, which is unexpected
349 t.Error("Process is not running")
350 } else {
351 // Expected: process should be running
352 t.Log("Process correctly running in background")
353 // Kill it for cleanup
354 process.Kill()
355 }
356
357 // Clean up
358 os.Remove(bgResult.StdoutFile)
359 os.Remove(bgResult.StderrFile)
360 os.Remove(filepath.Dir(bgResult.StdoutFile))
361 })
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000362}
363
364func TestBashTimeout(t *testing.T) {
365 // Test default timeout values
366 t.Run("Default Timeout Values", func(t *testing.T) {
367 // Test foreground default timeout
368 foreground := bashInput{
369 Command: "echo 'test'",
370 Background: false,
371 }
372 fgTimeout := foreground.timeout()
373 expectedFg := 1 * time.Minute
374 if fgTimeout != expectedFg {
375 t.Errorf("Expected foreground default timeout to be %v, got %v", expectedFg, fgTimeout)
376 }
377
378 // Test background default timeout
379 background := bashInput{
380 Command: "echo 'test'",
381 Background: true,
382 }
383 bgTimeout := background.timeout()
384 expectedBg := 10 * time.Minute
385 if bgTimeout != expectedBg {
386 t.Errorf("Expected background default timeout to be %v, got %v", expectedBg, bgTimeout)
387 }
388
389 // Test explicit timeout overrides defaults
390 explicit := bashInput{
391 Command: "echo 'test'",
392 Background: true,
393 Timeout: "5s",
394 }
395 explicitTimeout := explicit.timeout()
396 expectedExplicit := 5 * time.Second
397 if explicitTimeout != expectedExplicit {
398 t.Errorf("Expected explicit timeout to be %v, got %v", expectedExplicit, explicitTimeout)
399 }
400 })
401}
402
403// waitForFile waits for a file to exist and be non-empty or times out
404func waitForFile(t *testing.T, filepath string) {
405 timeout := time.After(5 * time.Second)
406 tick := time.NewTicker(10 * time.Millisecond)
407 defer tick.Stop()
408
409 for {
410 select {
411 case <-timeout:
412 t.Fatalf("Timed out waiting for file to exist and have contents: %s", filepath)
413 return
414 case <-tick.C:
415 info, err := os.Stat(filepath)
416 if err == nil && info.Size() > 0 {
417 return // File exists and has content
418 }
419 }
420 }
421}
422
423// waitForProcessDeath waits for a process to no longer exist or times out
424func waitForProcessDeath(t *testing.T, pid int) {
425 timeout := time.After(5 * time.Second)
426 tick := time.NewTicker(50 * time.Millisecond)
427 defer tick.Stop()
428
429 for {
430 select {
431 case <-timeout:
432 t.Fatalf("Timed out waiting for process %d to exit", pid)
433 return
434 case <-tick.C:
435 process, _ := os.FindProcess(pid)
436 err := process.Signal(syscall.Signal(0))
437 if err != nil {
438 // Process doesn't exist
439 return
440 }
441 }
442 }
443}