blob: 3865383027433942c45aa0292e5b594205bf8664 [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
Pokey Ruleb6d6d382025-05-07 10:29:03 +0100134 // Test SKETCH=1 environment variable is set
135 t.Run("SKETCH Environment Variable", func(t *testing.T) {
136 req := bashInput{
137 Command: "echo $SKETCH",
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 := "1\n"
147 if output != want {
148 t.Errorf("Expected SKETCH=1, got %q", output)
149 }
150 })
151
Earl Lee2e463fb2025-04-17 11:22:22 -0700152 // Test command with output to stderr
153 t.Run("Command with stderr", func(t *testing.T) {
154 req := bashInput{
155 Command: "echo 'Error message' >&2 && echo 'Success'",
156 Timeout: "5s",
157 }
158
159 output, err := executeBash(ctx, req)
160 if err != nil {
161 t.Fatalf("Unexpected error: %v", err)
162 }
163
164 want := "Error message\nSuccess\n"
165 if output != want {
166 t.Errorf("Expected %q, got %q", want, output)
167 }
168 })
169
170 // Test command that fails with stderr
171 t.Run("Failed Command with stderr", func(t *testing.T) {
172 req := bashInput{
173 Command: "echo 'Error message' >&2 && exit 1",
174 Timeout: "5s",
175 }
176
177 _, err := executeBash(ctx, req)
178 if err == nil {
179 t.Errorf("Expected error for failed command, got none")
180 } else if !strings.Contains(err.Error(), "Error message") {
181 t.Errorf("Expected stderr in error message, got: %v", err)
182 }
183 })
184
185 // Test timeout
186 t.Run("Command Timeout", func(t *testing.T) {
187 req := bashInput{
188 Command: "sleep 1 && echo 'Should not see this'",
189 Timeout: "100ms",
190 }
191
192 start := time.Now()
193 _, err := executeBash(ctx, req)
194 elapsed := time.Since(start)
195
196 // Command should time out after ~100ms, not wait for full 1 second
197 if elapsed >= 1*time.Second {
198 t.Errorf("Command did not respect timeout, took %v", elapsed)
199 }
200
201 if err == nil {
202 t.Errorf("Expected timeout error, got none")
203 } else if !strings.Contains(err.Error(), "timed out") {
204 t.Errorf("Expected timeout error, got: %v", err)
205 }
206 })
207}
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000208
209func TestBackgroundBash(t *testing.T) {
210 // Test basic background execution
211 t.Run("Basic Background Command", func(t *testing.T) {
212 inputObj := struct {
213 Command string `json:"command"`
214 Background bool `json:"background"`
215 }{
Pokey Ruleb6d6d382025-05-07 10:29:03 +0100216 Command: "echo 'Hello from background' $SKETCH",
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000217 Background: true,
218 }
219 inputJSON, err := json.Marshal(inputObj)
220 if err != nil {
221 t.Fatalf("Failed to marshal input: %v", err)
222 }
223
224 result, err := BashRun(context.Background(), inputJSON)
225 if err != nil {
226 t.Fatalf("Unexpected error: %v", err)
227 }
228
229 // Parse the returned JSON
230 var bgResult BackgroundResult
231 if err := json.Unmarshal([]byte(result), &bgResult); err != nil {
232 t.Fatalf("Failed to unmarshal background result: %v", err)
233 }
234
235 // Verify we got a valid PID
236 if bgResult.PID <= 0 {
237 t.Errorf("Invalid PID returned: %d", bgResult.PID)
238 }
239
240 // Verify output files exist
241 if _, err := os.Stat(bgResult.StdoutFile); os.IsNotExist(err) {
242 t.Errorf("Stdout file doesn't exist: %s", bgResult.StdoutFile)
243 }
244 if _, err := os.Stat(bgResult.StderrFile); os.IsNotExist(err) {
245 t.Errorf("Stderr file doesn't exist: %s", bgResult.StderrFile)
246 }
247
248 // Wait for the command output to be written to file
249 waitForFile(t, bgResult.StdoutFile)
250
251 // Check file contents
252 stdoutContent, err := os.ReadFile(bgResult.StdoutFile)
253 if err != nil {
254 t.Fatalf("Failed to read stdout file: %v", err)
255 }
Pokey Ruleb6d6d382025-05-07 10:29:03 +0100256 expected := "Hello from background 1\n"
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000257 if string(stdoutContent) != expected {
258 t.Errorf("Expected stdout content %q, got %q", expected, string(stdoutContent))
259 }
260
261 // Clean up
262 os.Remove(bgResult.StdoutFile)
263 os.Remove(bgResult.StderrFile)
264 os.Remove(filepath.Dir(bgResult.StdoutFile))
265 })
266
267 // Test background command with stderr output
268 t.Run("Background Command with stderr", func(t *testing.T) {
269 inputObj := struct {
270 Command string `json:"command"`
271 Background bool `json:"background"`
272 }{
273 Command: "echo 'Output to stdout' && echo 'Output to stderr' >&2",
274 Background: true,
275 }
276 inputJSON, err := json.Marshal(inputObj)
277 if err != nil {
278 t.Fatalf("Failed to marshal input: %v", err)
279 }
280
281 result, err := BashRun(context.Background(), inputJSON)
282 if err != nil {
283 t.Fatalf("Unexpected error: %v", err)
284 }
285
286 // Parse the returned JSON
287 var bgResult BackgroundResult
288 if err := json.Unmarshal([]byte(result), &bgResult); err != nil {
289 t.Fatalf("Failed to unmarshal background result: %v", err)
290 }
291
292 // Wait for the command output to be written to files
293 waitForFile(t, bgResult.StdoutFile)
294 waitForFile(t, bgResult.StderrFile)
295
296 // Check stdout content
297 stdoutContent, err := os.ReadFile(bgResult.StdoutFile)
298 if err != nil {
299 t.Fatalf("Failed to read stdout file: %v", err)
300 }
301 expectedStdout := "Output to stdout\n"
302 if string(stdoutContent) != expectedStdout {
303 t.Errorf("Expected stdout content %q, got %q", expectedStdout, string(stdoutContent))
304 }
305
306 // Check stderr content
307 stderrContent, err := os.ReadFile(bgResult.StderrFile)
308 if err != nil {
309 t.Fatalf("Failed to read stderr file: %v", err)
310 }
311 expectedStderr := "Output to stderr\n"
312 if string(stderrContent) != expectedStderr {
313 t.Errorf("Expected stderr content %q, got %q", expectedStderr, string(stderrContent))
314 }
315
316 // Clean up
317 os.Remove(bgResult.StdoutFile)
318 os.Remove(bgResult.StderrFile)
319 os.Remove(filepath.Dir(bgResult.StdoutFile))
320 })
321
322 // Test background command running without waiting
323 t.Run("Background Command Running", func(t *testing.T) {
324 // Create a script that will continue running after we check
325 inputObj := struct {
326 Command string `json:"command"`
327 Background bool `json:"background"`
328 }{
329 Command: "echo 'Running in background' && sleep 5",
330 Background: true,
331 }
332 inputJSON, err := json.Marshal(inputObj)
333 if err != nil {
334 t.Fatalf("Failed to marshal input: %v", err)
335 }
336
337 // Start the command in the background
338 result, err := BashRun(context.Background(), inputJSON)
339 if err != nil {
340 t.Fatalf("Unexpected error: %v", err)
341 }
342
343 // Parse the returned JSON
344 var bgResult BackgroundResult
345 if err := json.Unmarshal([]byte(result), &bgResult); err != nil {
346 t.Fatalf("Failed to unmarshal background result: %v", err)
347 }
348
349 // Wait for the command output to be written to file
350 waitForFile(t, bgResult.StdoutFile)
351
352 // Check stdout content
353 stdoutContent, err := os.ReadFile(bgResult.StdoutFile)
354 if err != nil {
355 t.Fatalf("Failed to read stdout file: %v", err)
356 }
357 expectedStdout := "Running in background\n"
358 if string(stdoutContent) != expectedStdout {
359 t.Errorf("Expected stdout content %q, got %q", expectedStdout, string(stdoutContent))
360 }
361
362 // Verify the process is still running
363 process, _ := os.FindProcess(bgResult.PID)
364 err = process.Signal(syscall.Signal(0))
365 if err != nil {
366 // Process not running, which is unexpected
367 t.Error("Process is not running")
368 } else {
369 // Expected: process should be running
370 t.Log("Process correctly running in background")
371 // Kill it for cleanup
372 process.Kill()
373 }
374
375 // Clean up
376 os.Remove(bgResult.StdoutFile)
377 os.Remove(bgResult.StderrFile)
378 os.Remove(filepath.Dir(bgResult.StdoutFile))
379 })
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000380}
381
382func TestBashTimeout(t *testing.T) {
383 // Test default timeout values
384 t.Run("Default Timeout Values", func(t *testing.T) {
385 // Test foreground default timeout
386 foreground := bashInput{
387 Command: "echo 'test'",
388 Background: false,
389 }
390 fgTimeout := foreground.timeout()
391 expectedFg := 1 * time.Minute
392 if fgTimeout != expectedFg {
393 t.Errorf("Expected foreground default timeout to be %v, got %v", expectedFg, fgTimeout)
394 }
395
396 // Test background default timeout
397 background := bashInput{
398 Command: "echo 'test'",
399 Background: true,
400 }
401 bgTimeout := background.timeout()
402 expectedBg := 10 * time.Minute
403 if bgTimeout != expectedBg {
404 t.Errorf("Expected background default timeout to be %v, got %v", expectedBg, bgTimeout)
405 }
406
407 // Test explicit timeout overrides defaults
408 explicit := bashInput{
409 Command: "echo 'test'",
410 Background: true,
411 Timeout: "5s",
412 }
413 explicitTimeout := explicit.timeout()
414 expectedExplicit := 5 * time.Second
415 if explicitTimeout != expectedExplicit {
416 t.Errorf("Expected explicit timeout to be %v, got %v", expectedExplicit, explicitTimeout)
417 }
418 })
419}
420
421// waitForFile waits for a file to exist and be non-empty or times out
422func waitForFile(t *testing.T, filepath string) {
423 timeout := time.After(5 * time.Second)
424 tick := time.NewTicker(10 * time.Millisecond)
425 defer tick.Stop()
426
427 for {
428 select {
429 case <-timeout:
430 t.Fatalf("Timed out waiting for file to exist and have contents: %s", filepath)
431 return
432 case <-tick.C:
433 info, err := os.Stat(filepath)
434 if err == nil && info.Size() > 0 {
435 return // File exists and has content
436 }
437 }
438 }
439}
440
441// waitForProcessDeath waits for a process to no longer exist or times out
442func waitForProcessDeath(t *testing.T, pid int) {
443 timeout := time.After(5 * time.Second)
444 tick := time.NewTicker(50 * time.Millisecond)
445 defer tick.Stop()
446
447 for {
448 select {
449 case <-timeout:
450 t.Fatalf("Timed out waiting for process %d to exit", pid)
451 return
452 case <-tick.C:
453 process, _ := os.FindProcess(pid)
454 err := process.Signal(syscall.Signal(0))
455 if err != nil {
456 // Process doesn't exist
457 return
458 }
459 }
460 }
461}