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