blob: 4f0a6254c0fb61bcb4113910ad91037a3c52a822 [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
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000014func TestBashSlowOk(t *testing.T) {
15 // Test that slow_ok flag is properly handled
16 t.Run("SlowOk Flag", func(t *testing.T) {
17 input := json.RawMessage(`{"command":"echo 'slow test'","slow_ok":true}`)
18
19 bashTool := (&BashTool{}).Tool()
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -070020 toolOut := bashTool.Run(context.Background(), input)
21 if toolOut.Error != nil {
22 t.Fatalf("Unexpected error: %v", toolOut.Error)
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000023 }
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -070024 result := toolOut.LLMContent
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000025
26 expected := "slow test\n"
27 if len(result) == 0 || result[0].Text != expected {
28 t.Errorf("Expected %q, got %q", expected, result[0].Text)
29 }
30 })
31
32 // Test that slow_ok with background works
33 t.Run("SlowOk with Background", func(t *testing.T) {
34 input := json.RawMessage(`{"command":"echo 'slow background test'","slow_ok":true,"background":true}`)
35
36 bashTool := (&BashTool{}).Tool()
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -070037 toolOut := bashTool.Run(context.Background(), input)
38 if toolOut.Error != nil {
39 t.Fatalf("Unexpected error: %v", toolOut.Error)
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000040 }
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -070041 result := toolOut.LLMContent
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000042
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -070043 // Should return background result XML-ish format
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000044 resultStr := result[0].Text
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -070045 if !strings.Contains(resultStr, "<pid>") || !strings.Contains(resultStr, "<output_file>") {
46 t.Errorf("Expected XML-ish background result format, got: %s", resultStr)
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000047 }
48
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -070049 // Extract PID and output file from XML-ish format for cleanup
50 // This is a simple extraction for test cleanup - in real usage the agent would parse this
51 lines := strings.Split(resultStr, "\n")
52 var outFile string
53 for _, line := range lines {
54 if strings.Contains(line, "<output_file>") {
55 start := strings.Index(line, "<output_file>") + len("<output_file>")
56 end := strings.Index(line, "</output_file>")
57 if end > start {
58 outFile = line[start:end]
59 }
60 break
61 }
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000062 }
63
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -070064 if outFile != "" {
65 // Clean up
66 os.Remove(outFile)
67 os.Remove(filepath.Dir(outFile))
68 }
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000069 })
70}
71
Philip Zeyligercfd0fe62025-06-21 02:17:41 +000072func TestBashTool(t *testing.T) {
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +000073 var bashTool BashTool
74 tool := bashTool.Tool()
75
Earl Lee2e463fb2025-04-17 11:22:22 -070076 // Test basic functionality
77 t.Run("Basic Command", func(t *testing.T) {
78 input := json.RawMessage(`{"command":"echo 'Hello, world!'"}`)
79
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -070080 toolOut := tool.Run(context.Background(), input)
81 if toolOut.Error != nil {
82 t.Fatalf("Unexpected error: %v", toolOut.Error)
Earl Lee2e463fb2025-04-17 11:22:22 -070083 }
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -070084 result := toolOut.LLMContent
Earl Lee2e463fb2025-04-17 11:22:22 -070085
86 expected := "Hello, world!\n"
Philip Zeyligercfd0fe62025-06-21 02:17:41 +000087 if len(result) == 0 || result[0].Text != expected {
88 t.Errorf("Expected %q, got %q", expected, result[0].Text)
Earl Lee2e463fb2025-04-17 11:22:22 -070089 }
90 })
91
92 // Test with arguments
93 t.Run("Command With Arguments", func(t *testing.T) {
94 input := json.RawMessage(`{"command":"echo -n foo && echo -n bar"}`)
95
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -070096 toolOut := tool.Run(context.Background(), input)
97 if toolOut.Error != nil {
98 t.Fatalf("Unexpected error: %v", toolOut.Error)
Earl Lee2e463fb2025-04-17 11:22:22 -070099 }
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -0700100 result := toolOut.LLMContent
Earl Lee2e463fb2025-04-17 11:22:22 -0700101
102 expected := "foobar"
Philip Zeyligercfd0fe62025-06-21 02:17:41 +0000103 if len(result) == 0 || result[0].Text != expected {
104 t.Errorf("Expected %q, got %q", expected, result[0].Text)
Earl Lee2e463fb2025-04-17 11:22:22 -0700105 }
106 })
107
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000108 // Test with slow_ok parameter
109 t.Run("With SlowOK", func(t *testing.T) {
Earl Lee2e463fb2025-04-17 11:22:22 -0700110 inputObj := struct {
111 Command string `json:"command"`
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000112 SlowOK bool `json:"slow_ok"`
Earl Lee2e463fb2025-04-17 11:22:22 -0700113 }{
114 Command: "sleep 0.1 && echo 'Completed'",
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000115 SlowOK: true,
Earl Lee2e463fb2025-04-17 11:22:22 -0700116 }
117 inputJSON, err := json.Marshal(inputObj)
118 if err != nil {
119 t.Fatalf("Failed to marshal input: %v", err)
120 }
121
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -0700122 toolOut := tool.Run(context.Background(), inputJSON)
123 if toolOut.Error != nil {
124 t.Fatalf("Unexpected error: %v", toolOut.Error)
Earl Lee2e463fb2025-04-17 11:22:22 -0700125 }
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -0700126 result := toolOut.LLMContent
Earl Lee2e463fb2025-04-17 11:22:22 -0700127
128 expected := "Completed\n"
Philip Zeyligercfd0fe62025-06-21 02:17:41 +0000129 if len(result) == 0 || result[0].Text != expected {
130 t.Errorf("Expected %q, got %q", expected, result[0].Text)
Earl Lee2e463fb2025-04-17 11:22:22 -0700131 }
132 })
133
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000134 // Test command timeout with custom timeout config
Earl Lee2e463fb2025-04-17 11:22:22 -0700135 t.Run("Command Timeout", func(t *testing.T) {
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000136 // Use a custom BashTool with very short timeout
137 customTimeouts := &Timeouts{
138 Fast: 100 * time.Millisecond,
139 Slow: 100 * time.Millisecond,
140 Background: 100 * time.Millisecond,
Earl Lee2e463fb2025-04-17 11:22:22 -0700141 }
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000142 customBash := &BashTool{
143 Timeouts: customTimeouts,
Earl Lee2e463fb2025-04-17 11:22:22 -0700144 }
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000145 tool := customBash.Tool()
Earl Lee2e463fb2025-04-17 11:22:22 -0700146
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000147 input := json.RawMessage(`{"command":"sleep 0.5 && echo 'Should not see this'"}`)
148
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -0700149 toolOut := tool.Run(context.Background(), input)
150 if toolOut.Error == nil {
Earl Lee2e463fb2025-04-17 11:22:22 -0700151 t.Errorf("Expected timeout error, got none")
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -0700152 } else if !strings.Contains(toolOut.Error.Error(), "timed out") {
153 t.Errorf("Expected timeout error, got: %v", toolOut.Error)
Earl Lee2e463fb2025-04-17 11:22:22 -0700154 }
155 })
156
157 // Test command that fails
158 t.Run("Failed Command", func(t *testing.T) {
159 input := json.RawMessage(`{"command":"exit 1"}`)
160
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -0700161 toolOut := tool.Run(context.Background(), input)
162 if toolOut.Error == nil {
Earl Lee2e463fb2025-04-17 11:22:22 -0700163 t.Errorf("Expected error for failed command, got none")
164 }
165 })
166
167 // Test invalid input
168 t.Run("Invalid JSON Input", func(t *testing.T) {
169 input := json.RawMessage(`{"command":123}`) // Invalid JSON (command must be string)
170
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -0700171 toolOut := tool.Run(context.Background(), input)
172 if toolOut.Error == nil {
Earl Lee2e463fb2025-04-17 11:22:22 -0700173 t.Errorf("Expected error for invalid input, got none")
174 }
175 })
176}
177
178func TestExecuteBash(t *testing.T) {
179 ctx := context.Background()
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700180 bashTool := &BashTool{}
Earl Lee2e463fb2025-04-17 11:22:22 -0700181
182 // Test successful command
183 t.Run("Successful Command", func(t *testing.T) {
184 req := bashInput{
185 Command: "echo 'Success'",
Earl Lee2e463fb2025-04-17 11:22:22 -0700186 }
187
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700188 output, err := bashTool.executeBash(ctx, req, 5*time.Second)
Earl Lee2e463fb2025-04-17 11:22:22 -0700189 if err != nil {
190 t.Fatalf("Unexpected error: %v", err)
191 }
192
193 want := "Success\n"
194 if output != want {
195 t.Errorf("Expected %q, got %q", want, output)
196 }
197 })
198
Pokey Ruleb6d6d382025-05-07 10:29:03 +0100199 // Test SKETCH=1 environment variable is set
200 t.Run("SKETCH Environment Variable", func(t *testing.T) {
201 req := bashInput{
202 Command: "echo $SKETCH",
Pokey Ruleb6d6d382025-05-07 10:29:03 +0100203 }
204
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700205 output, err := bashTool.executeBash(ctx, req, 5*time.Second)
Pokey Ruleb6d6d382025-05-07 10:29:03 +0100206 if err != nil {
207 t.Fatalf("Unexpected error: %v", err)
208 }
209
210 want := "1\n"
211 if output != want {
212 t.Errorf("Expected SKETCH=1, got %q", output)
213 }
214 })
215
Earl Lee2e463fb2025-04-17 11:22:22 -0700216 // Test command with output to stderr
217 t.Run("Command with stderr", func(t *testing.T) {
218 req := bashInput{
219 Command: "echo 'Error message' >&2 && echo 'Success'",
Earl Lee2e463fb2025-04-17 11:22:22 -0700220 }
221
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700222 output, err := bashTool.executeBash(ctx, req, 5*time.Second)
Earl Lee2e463fb2025-04-17 11:22:22 -0700223 if err != nil {
224 t.Fatalf("Unexpected error: %v", err)
225 }
226
227 want := "Error message\nSuccess\n"
228 if output != want {
229 t.Errorf("Expected %q, got %q", want, output)
230 }
231 })
232
233 // Test command that fails with stderr
234 t.Run("Failed Command with stderr", func(t *testing.T) {
235 req := bashInput{
236 Command: "echo 'Error message' >&2 && exit 1",
Earl Lee2e463fb2025-04-17 11:22:22 -0700237 }
238
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700239 _, err := bashTool.executeBash(ctx, req, 5*time.Second)
Earl Lee2e463fb2025-04-17 11:22:22 -0700240 if err == nil {
241 t.Errorf("Expected error for failed command, got none")
242 } else if !strings.Contains(err.Error(), "Error message") {
243 t.Errorf("Expected stderr in error message, got: %v", err)
244 }
245 })
246
247 // Test timeout
248 t.Run("Command Timeout", func(t *testing.T) {
249 req := bashInput{
250 Command: "sleep 1 && echo 'Should not see this'",
Earl Lee2e463fb2025-04-17 11:22:22 -0700251 }
252
253 start := time.Now()
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700254 _, err := bashTool.executeBash(ctx, req, 100*time.Millisecond)
Earl Lee2e463fb2025-04-17 11:22:22 -0700255 elapsed := time.Since(start)
256
257 // Command should time out after ~100ms, not wait for full 1 second
258 if elapsed >= 1*time.Second {
259 t.Errorf("Command did not respect timeout, took %v", elapsed)
260 }
261
262 if err == nil {
263 t.Errorf("Expected timeout error, got none")
264 } else if !strings.Contains(err.Error(), "timed out") {
265 t.Errorf("Expected timeout error, got: %v", err)
266 }
267 })
268}
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000269
270func TestBackgroundBash(t *testing.T) {
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000271 var bashTool BashTool
272 tool := bashTool.Tool()
273
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000274 // Test basic background execution
275 t.Run("Basic Background Command", func(t *testing.T) {
276 inputObj := struct {
277 Command string `json:"command"`
278 Background bool `json:"background"`
279 }{
Pokey Ruleb6d6d382025-05-07 10:29:03 +0100280 Command: "echo 'Hello from background' $SKETCH",
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000281 Background: true,
282 }
283 inputJSON, err := json.Marshal(inputObj)
284 if err != nil {
285 t.Fatalf("Failed to marshal input: %v", err)
286 }
287
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -0700288 toolOut := tool.Run(context.Background(), inputJSON)
289 if toolOut.Error != nil {
290 t.Fatalf("Unexpected error: %v", toolOut.Error)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000291 }
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -0700292 result := toolOut.LLMContent
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000293
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700294 // Parse the returned XML-ish format
Philip Zeyligercfd0fe62025-06-21 02:17:41 +0000295 resultStr := result[0].Text
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700296 if !strings.Contains(resultStr, "<pid>") || !strings.Contains(resultStr, "<output_file>") {
297 t.Fatalf("Expected XML-ish background result format, got: %s", resultStr)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000298 }
299
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700300 // Extract PID and output file from XML-ish format
301 lines := strings.Split(resultStr, "\n")
302 var pidStr, outFile string
303 for _, line := range lines {
304 if strings.Contains(line, "<pid>") {
305 start := strings.Index(line, "<pid>") + len("<pid>")
306 end := strings.Index(line, "</pid>")
307 if end > start {
308 pidStr = line[start:end]
309 }
310 } else if strings.Contains(line, "<output_file>") {
311 start := strings.Index(line, "<output_file>") + len("<output_file>")
312 end := strings.Index(line, "</output_file>")
313 if end > start {
314 outFile = line[start:end]
315 }
316 }
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000317 }
318
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700319 // Verify we got valid values
320 if pidStr == "" || outFile == "" {
321 t.Errorf("Failed to extract PID or output file from result: %s", resultStr)
322 return
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000323 }
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700324
325 // Verify output file exists
326 if _, err := os.Stat(outFile); os.IsNotExist(err) {
327 t.Errorf("Output file doesn't exist: %s", outFile)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000328 }
329
330 // Wait for the command output to be written to file
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700331 waitForFile(t, outFile)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000332
333 // Check file contents
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700334 outputContent, err := os.ReadFile(outFile)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000335 if err != nil {
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700336 t.Fatalf("Failed to read output file: %v", err)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000337 }
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700338 // The implementation appends a completion message to the output
339 outputStr := string(outputContent)
340 if !strings.Contains(outputStr, "Hello from background 1") {
341 t.Errorf("Expected output to contain 'Hello from background 1', got %q", outputStr)
342 }
343 if !strings.Contains(outputStr, "[background process completed]") {
344 t.Errorf("Expected output to contain completion message, got %q", outputStr)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000345 }
346
347 // Clean up
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700348 os.Remove(outFile)
349 os.Remove(filepath.Dir(outFile))
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000350 })
351
352 // Test background command with stderr output
353 t.Run("Background Command with stderr", func(t *testing.T) {
354 inputObj := struct {
355 Command string `json:"command"`
356 Background bool `json:"background"`
357 }{
358 Command: "echo 'Output to stdout' && echo 'Output to stderr' >&2",
359 Background: true,
360 }
361 inputJSON, err := json.Marshal(inputObj)
362 if err != nil {
363 t.Fatalf("Failed to marshal input: %v", err)
364 }
365
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -0700366 toolOut := tool.Run(context.Background(), inputJSON)
367 if toolOut.Error != nil {
368 t.Fatalf("Unexpected error: %v", toolOut.Error)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000369 }
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -0700370 result := toolOut.LLMContent
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000371
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700372 // Parse the returned XML-ish format
Philip Zeyligercfd0fe62025-06-21 02:17:41 +0000373 resultStr := result[0].Text
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700374 lines := strings.Split(resultStr, "\n")
375 var outFile string
376 for _, line := range lines {
377 if strings.Contains(line, "<output_file>") {
378 start := strings.Index(line, "<output_file>") + len("<output_file>")
379 end := strings.Index(line, "</output_file>")
380 if end > start {
381 outFile = line[start:end]
382 }
383 break
384 }
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000385 }
386
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700387 // Wait for the command output to be written to file
388 waitForFile(t, outFile)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000389
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700390 // Check output content (stdout and stderr are combined in implementation)
391 outputContent, err := os.ReadFile(outFile)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000392 if err != nil {
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700393 t.Fatalf("Failed to read output file: %v", err)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000394 }
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700395 // Implementation combines stdout and stderr into one file
396 outputStr := string(outputContent)
397 if !strings.Contains(outputStr, "Output to stdout") || !strings.Contains(outputStr, "Output to stderr") {
398 t.Errorf("Expected both stdout and stderr content, got %q", outputStr)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000399 }
400
401 // Clean up
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700402 os.Remove(outFile)
403 os.Remove(filepath.Dir(outFile))
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000404 })
405
406 // Test background command running without waiting
407 t.Run("Background Command Running", func(t *testing.T) {
408 // Create a script that will continue running after we check
409 inputObj := struct {
410 Command string `json:"command"`
411 Background bool `json:"background"`
412 }{
413 Command: "echo 'Running in background' && sleep 5",
414 Background: true,
415 }
416 inputJSON, err := json.Marshal(inputObj)
417 if err != nil {
418 t.Fatalf("Failed to marshal input: %v", err)
419 }
420
421 // Start the command in the background
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -0700422 toolOut := tool.Run(context.Background(), inputJSON)
423 if toolOut.Error != nil {
424 t.Fatalf("Unexpected error: %v", toolOut.Error)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000425 }
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -0700426 result := toolOut.LLMContent
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000427
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700428 // Parse the returned XML-ish format
Philip Zeyligercfd0fe62025-06-21 02:17:41 +0000429 resultStr := result[0].Text
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700430 lines := strings.Split(resultStr, "\n")
431 var pidStr, outFile string
432 for _, line := range lines {
433 if strings.Contains(line, "<pid>") {
434 start := strings.Index(line, "<pid>") + len("<pid>")
435 end := strings.Index(line, "</pid>")
436 if end > start {
437 pidStr = line[start:end]
438 }
439 } else if strings.Contains(line, "<output_file>") {
440 start := strings.Index(line, "<output_file>") + len("<output_file>")
441 end := strings.Index(line, "</output_file>")
442 if end > start {
443 outFile = line[start:end]
444 }
445 }
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000446 }
447
448 // Wait for the command output to be written to file
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700449 waitForFile(t, outFile)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000450
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700451 // Check output content
452 outputContent, err := os.ReadFile(outFile)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000453 if err != nil {
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700454 t.Fatalf("Failed to read output file: %v", err)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000455 }
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700456 expectedOutput := "Running in background\n"
457 if string(outputContent) != expectedOutput {
458 t.Errorf("Expected output content %q, got %q", expectedOutput, string(outputContent))
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000459 }
460
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700461 // Verify the process is still running by parsing PID
462 if pidStr != "" {
463 // We can't easily test if the process is still running without importing strconv
464 // and the process might have finished by now anyway due to timing
465 t.Log("Process started in background with PID:", pidStr)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000466 }
467
468 // Clean up
Josh Bleecher Snyder6fe809c2025-07-24 16:22:51 -0700469 os.Remove(outFile)
470 os.Remove(filepath.Dir(outFile))
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000471 })
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000472}
473
474func TestBashTimeout(t *testing.T) {
475 // Test default timeout values
476 t.Run("Default Timeout Values", func(t *testing.T) {
477 // Test foreground default timeout
478 foreground := bashInput{
479 Command: "echo 'test'",
480 Background: false,
481 }
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000482 fgTimeout := foreground.timeout(nil)
483 expectedFg := 30 * time.Second
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000484 if fgTimeout != expectedFg {
485 t.Errorf("Expected foreground default timeout to be %v, got %v", expectedFg, fgTimeout)
486 }
487
488 // Test background default timeout
489 background := bashInput{
490 Command: "echo 'test'",
491 Background: true,
492 }
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000493 bgTimeout := background.timeout(nil)
494 expectedBg := 24 * time.Hour
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000495 if bgTimeout != expectedBg {
496 t.Errorf("Expected background default timeout to be %v, got %v", expectedBg, bgTimeout)
497 }
498
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000499 // Test slow_ok timeout
500 slowOk := bashInput{
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000501 Command: "echo 'test'",
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000502 Background: false,
503 SlowOK: true,
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000504 }
Josh Bleecher Snyder17b2fd92025-07-09 22:47:13 +0000505 slowTimeout := slowOk.timeout(nil)
506 expectedSlow := 15 * time.Minute
507 if slowTimeout != expectedSlow {
508 t.Errorf("Expected slow_ok timeout to be %v, got %v", expectedSlow, slowTimeout)
509 }
510
511 // Test custom timeout config
512 customTimeouts := &Timeouts{
513 Fast: 5 * time.Second,
514 Slow: 2 * time.Minute,
515 Background: 1 * time.Hour,
516 }
517 customFast := bashInput{
518 Command: "echo 'test'",
519 Background: false,
520 }
521 customTimeout := customFast.timeout(customTimeouts)
522 expectedCustom := 5 * time.Second
523 if customTimeout != expectedCustom {
524 t.Errorf("Expected custom timeout to be %v, got %v", expectedCustom, customTimeout)
Philip Zeyligerb60f0f22025-04-23 18:19:32 +0000525 }
526 })
527}
528
529// waitForFile waits for a file to exist and be non-empty or times out
530func waitForFile(t *testing.T, filepath string) {
531 timeout := time.After(5 * time.Second)
532 tick := time.NewTicker(10 * time.Millisecond)
533 defer tick.Stop()
534
535 for {
536 select {
537 case <-timeout:
538 t.Fatalf("Timed out waiting for file to exist and have contents: %s", filepath)
539 return
540 case <-tick.C:
541 info, err := os.Stat(filepath)
542 if err == nil && info.Size() > 0 {
543 return // File exists and has content
544 }
545 }
546 }
547}
548
549// waitForProcessDeath waits for a process to no longer exist or times out
550func waitForProcessDeath(t *testing.T, pid int) {
551 timeout := time.After(5 * time.Second)
552 tick := time.NewTicker(50 * time.Millisecond)
553 defer tick.Stop()
554
555 for {
556 select {
557 case <-timeout:
558 t.Fatalf("Timed out waiting for process %d to exit", pid)
559 return
560 case <-tick.C:
561 process, _ := os.FindProcess(pid)
562 err := process.Signal(syscall.Signal(0))
563 if err != nil {
564 // Process doesn't exist
565 return
566 }
567 }
568 }
569}