bash_test.go

  1package claudetool
  2
  3import (
  4	"context"
  5	"encoding/json"
  6	"os"
  7	"os/exec"
  8	"path/filepath"
  9	"strings"
 10	"syscall"
 11	"testing"
 12	"time"
 13)
 14
 15func TestBashSlowOk(t *testing.T) {
 16	// Test that slow_ok flag is properly handled
 17	t.Run("SlowOk Flag", func(t *testing.T) {
 18		input := json.RawMessage(`{"command":"echo 'slow test'","slow_ok":true}`)
 19
 20		bashTool := (&BashTool{WorkingDir: NewMutableWorkingDir("/")}).Tool()
 21		toolOut := bashTool.Run(context.Background(), input)
 22		if toolOut.Error != nil {
 23			t.Fatalf("Unexpected error: %v", toolOut.Error)
 24		}
 25		result := toolOut.LLMContent
 26
 27		expected := "slow test\n"
 28		if len(result) == 0 || result[0].Text != expected {
 29			t.Errorf("Expected %q, got %q", expected, result[0].Text)
 30		}
 31	})
 32
 33	// Test that slow_ok with background works
 34	t.Run("SlowOk with Background", func(t *testing.T) {
 35		input := json.RawMessage(`{"command":"echo 'slow background test'","slow_ok":true,"background":true}`)
 36
 37		bashTool := (&BashTool{WorkingDir: NewMutableWorkingDir("/")}).Tool()
 38		toolOut := bashTool.Run(context.Background(), input)
 39		if toolOut.Error != nil {
 40			t.Fatalf("Unexpected error: %v", toolOut.Error)
 41		}
 42		result := toolOut.LLMContent
 43
 44		// Should return background result XML-ish format
 45		resultStr := result[0].Text
 46		if !strings.Contains(resultStr, "<pid>") || !strings.Contains(resultStr, "<output_file>") {
 47			t.Errorf("Expected XML-ish background result format, got: %s", resultStr)
 48		}
 49
 50		// Extract PID and output file from XML-ish format for cleanup
 51		// This is a simple extraction for test cleanup - in real usage the agent would parse this
 52		lines := strings.Split(resultStr, "\n")
 53		var outFile string
 54		for _, line := range lines {
 55			if strings.Contains(line, "<output_file>") {
 56				start := strings.Index(line, "<output_file>") + len("<output_file>")
 57				end := strings.Index(line, "</output_file>")
 58				if end > start {
 59					outFile = line[start:end]
 60				}
 61				break
 62			}
 63		}
 64
 65		if outFile != "" {
 66			// Clean up
 67			os.Remove(outFile)
 68			os.Remove(filepath.Dir(outFile))
 69		}
 70	})
 71}
 72
 73func TestBashTool(t *testing.T) {
 74	bashTool := &BashTool{WorkingDir: NewMutableWorkingDir("/")}
 75	tool := bashTool.Tool()
 76
 77	// Test basic functionality
 78	t.Run("Basic Command", func(t *testing.T) {
 79		input := json.RawMessage(`{"command":"echo 'Hello, world!'"}`)
 80
 81		toolOut := tool.Run(context.Background(), input)
 82		if toolOut.Error != nil {
 83			t.Fatalf("Unexpected error: %v", toolOut.Error)
 84		}
 85		result := toolOut.LLMContent
 86
 87		expected := "Hello, world!\n"
 88		if len(result) == 0 || result[0].Text != expected {
 89			t.Errorf("Expected %q, got %q", expected, result[0].Text)
 90		}
 91
 92		// Verify Display data contains working directory
 93		display, ok := toolOut.Display.(BashDisplayData)
 94		if !ok {
 95			t.Fatalf("Expected Display to be BashDisplayData, got %T", toolOut.Display)
 96		}
 97		if display.WorkingDir != "/" {
 98			t.Errorf("Expected WorkingDir to be '/', got %q", display.WorkingDir)
 99		}
100	})
101
102	// Test with arguments
103	t.Run("Command With Arguments", func(t *testing.T) {
104		input := json.RawMessage(`{"command":"echo -n foo && echo -n bar"}`)
105
106		toolOut := tool.Run(context.Background(), input)
107		if toolOut.Error != nil {
108			t.Fatalf("Unexpected error: %v", toolOut.Error)
109		}
110		result := toolOut.LLMContent
111
112		expected := "foobar"
113		if len(result) == 0 || result[0].Text != expected {
114			t.Errorf("Expected %q, got %q", expected, result[0].Text)
115		}
116	})
117
118	// Test with slow_ok parameter
119	t.Run("With SlowOK", func(t *testing.T) {
120		inputObj := struct {
121			Command string `json:"command"`
122			SlowOK  bool   `json:"slow_ok"`
123		}{
124			Command: "sleep 0.1 && echo 'Completed'",
125			SlowOK:  true,
126		}
127		inputJSON, err := json.Marshal(inputObj)
128		if err != nil {
129			t.Fatalf("Failed to marshal input: %v", err)
130		}
131
132		toolOut := tool.Run(context.Background(), inputJSON)
133		if toolOut.Error != nil {
134			t.Fatalf("Unexpected error: %v", toolOut.Error)
135		}
136		result := toolOut.LLMContent
137
138		expected := "Completed\n"
139		if len(result) == 0 || result[0].Text != expected {
140			t.Errorf("Expected %q, got %q", expected, result[0].Text)
141		}
142	})
143
144	// Test command timeout with custom timeout config
145	t.Run("Command Timeout", func(t *testing.T) {
146		// Use a custom BashTool with very short timeout
147		customTimeouts := &Timeouts{
148			Fast:       100 * time.Millisecond,
149			Slow:       100 * time.Millisecond,
150			Background: 100 * time.Millisecond,
151		}
152		customBash := &BashTool{
153			WorkingDir: NewMutableWorkingDir("/"),
154			Timeouts:   customTimeouts,
155		}
156		tool := customBash.Tool()
157
158		input := json.RawMessage(`{"command":"sleep 0.5 && echo 'Should not see this'"}`)
159
160		toolOut := tool.Run(context.Background(), input)
161		if toolOut.Error == nil {
162			t.Errorf("Expected timeout error, got none")
163		} else if !strings.Contains(toolOut.Error.Error(), "timed out") {
164			t.Errorf("Expected timeout error, got: %v", toolOut.Error)
165		}
166	})
167
168	// Test command that fails
169	t.Run("Failed Command", func(t *testing.T) {
170		input := json.RawMessage(`{"command":"exit 1"}`)
171
172		toolOut := tool.Run(context.Background(), input)
173		if toolOut.Error == nil {
174			t.Errorf("Expected error for failed command, got none")
175		}
176	})
177
178	// Test invalid input
179	t.Run("Invalid JSON Input", func(t *testing.T) {
180		input := json.RawMessage(`{"command":123}`) // Invalid JSON (command must be string)
181
182		toolOut := tool.Run(context.Background(), input)
183		if toolOut.Error == nil {
184			t.Errorf("Expected error for invalid input, got none")
185		}
186	})
187}
188
189func TestExecuteBash(t *testing.T) {
190	ctx := context.Background()
191	bashTool := &BashTool{WorkingDir: NewMutableWorkingDir("/")}
192
193	// Test successful command
194	t.Run("Successful Command", func(t *testing.T) {
195		req := bashInput{
196			Command: "echo 'Success'",
197		}
198
199		output, err := bashTool.executeBash(ctx, req, 5*time.Second)
200		if err != nil {
201			t.Fatalf("Unexpected error: %v", err)
202		}
203
204		want := "Success\n"
205		if output != want {
206			t.Errorf("Expected %q, got %q", want, output)
207		}
208	})
209
210	// Test SKETCH=1 environment variable is set
211	t.Run("SKETCH Environment Variable", func(t *testing.T) {
212		req := bashInput{
213			Command: "echo $SKETCH",
214		}
215
216		output, err := bashTool.executeBash(ctx, req, 5*time.Second)
217		if err != nil {
218			t.Fatalf("Unexpected error: %v", err)
219		}
220
221		want := "1\n"
222		if output != want {
223			t.Errorf("Expected SKETCH=1, got %q", output)
224		}
225	})
226
227	// Test SHELLEY_CONVERSATION_ID environment variable is set when configured
228	t.Run("SHELLEY_CONVERSATION_ID Environment Variable", func(t *testing.T) {
229		bashWithConvID := &BashTool{
230			WorkingDir:     NewMutableWorkingDir("/"),
231			ConversationID: "test-conv-123",
232		}
233		req := bashInput{
234			Command: "echo $SHELLEY_CONVERSATION_ID",
235		}
236
237		output, err := bashWithConvID.executeBash(ctx, req, 5*time.Second)
238		if err != nil {
239			t.Fatalf("Unexpected error: %v", err)
240		}
241
242		want := "test-conv-123\n"
243		if output != want {
244			t.Errorf("Expected SHELLEY_CONVERSATION_ID=test-conv-123, got %q", output)
245		}
246	})
247
248	// Test SHELLEY_CONVERSATION_ID is not set when not configured
249	t.Run("SHELLEY_CONVERSATION_ID Not Set When Empty", func(t *testing.T) {
250		req := bashInput{
251			Command: "echo \"conv_id:$SHELLEY_CONVERSATION_ID:\"",
252		}
253
254		output, err := bashTool.executeBash(ctx, req, 5*time.Second)
255		if err != nil {
256			t.Fatalf("Unexpected error: %v", err)
257		}
258
259		// Should be empty since ConversationID is not set on bashTool
260		want := "conv_id::\n"
261		if output != want {
262			t.Errorf("Expected empty SHELLEY_CONVERSATION_ID, got %q", output)
263		}
264	})
265
266	// Test command with output to stderr
267	t.Run("Command with stderr", func(t *testing.T) {
268		req := bashInput{
269			Command: "echo 'Error message' >&2 && echo 'Success'",
270		}
271
272		output, err := bashTool.executeBash(ctx, req, 5*time.Second)
273		if err != nil {
274			t.Fatalf("Unexpected error: %v", err)
275		}
276
277		want := "Error message\nSuccess\n"
278		if output != want {
279			t.Errorf("Expected %q, got %q", want, output)
280		}
281	})
282
283	// Test command that fails with stderr
284	t.Run("Failed Command with stderr", func(t *testing.T) {
285		req := bashInput{
286			Command: "echo 'Error message' >&2 && exit 1",
287		}
288
289		_, err := bashTool.executeBash(ctx, req, 5*time.Second)
290		if err == nil {
291			t.Errorf("Expected error for failed command, got none")
292		} else if !strings.Contains(err.Error(), "Error message") {
293			t.Errorf("Expected stderr in error message, got: %v", err)
294		}
295	})
296
297	// Test timeout
298	t.Run("Command Timeout", func(t *testing.T) {
299		req := bashInput{
300			Command: "sleep 1 && echo 'Should not see this'",
301		}
302
303		start := time.Now()
304		_, err := bashTool.executeBash(ctx, req, 100*time.Millisecond)
305		elapsed := time.Since(start)
306
307		// Command should time out after ~100ms, not wait for full 1 second
308		if elapsed >= 1*time.Second {
309			t.Errorf("Command did not respect timeout, took %v", elapsed)
310		}
311
312		if err == nil {
313			t.Errorf("Expected timeout error, got none")
314		} else if !strings.Contains(err.Error(), "timed out") {
315			t.Errorf("Expected timeout error, got: %v", err)
316		}
317	})
318}
319
320func TestBackgroundBash(t *testing.T) {
321	bashTool := &BashTool{WorkingDir: NewMutableWorkingDir("/")}
322	tool := bashTool.Tool()
323
324	// Test basic background execution
325	t.Run("Basic Background Command", func(t *testing.T) {
326		inputObj := struct {
327			Command    string `json:"command"`
328			Background bool   `json:"background"`
329		}{
330			Command:    "echo 'Hello from background' $SKETCH",
331			Background: true,
332		}
333		inputJSON, err := json.Marshal(inputObj)
334		if err != nil {
335			t.Fatalf("Failed to marshal input: %v", err)
336		}
337
338		toolOut := tool.Run(context.Background(), inputJSON)
339		if toolOut.Error != nil {
340			t.Fatalf("Unexpected error: %v", toolOut.Error)
341		}
342		result := toolOut.LLMContent
343
344		// Parse the returned XML-ish format
345		resultStr := result[0].Text
346		if !strings.Contains(resultStr, "<pid>") || !strings.Contains(resultStr, "<output_file>") {
347			t.Fatalf("Expected XML-ish background result format, got: %s", resultStr)
348		}
349
350		// Extract PID and output file from XML-ish format
351		lines := strings.Split(resultStr, "\n")
352		var pidStr, outFile string
353		for _, line := range lines {
354			if strings.Contains(line, "<pid>") {
355				start := strings.Index(line, "<pid>") + len("<pid>")
356				end := strings.Index(line, "</pid>")
357				if end > start {
358					pidStr = line[start:end]
359				}
360			} else if strings.Contains(line, "<output_file>") {
361				start := strings.Index(line, "<output_file>") + len("<output_file>")
362				end := strings.Index(line, "</output_file>")
363				if end > start {
364					outFile = line[start:end]
365				}
366			}
367		}
368
369		// Verify we got valid values
370		if pidStr == "" || outFile == "" {
371			t.Errorf("Failed to extract PID or output file from result: %s", resultStr)
372			return
373		}
374
375		// Verify output file exists
376		if _, err := os.Stat(outFile); os.IsNotExist(err) {
377			t.Errorf("Output file doesn't exist: %s", outFile)
378		}
379
380		// Wait for the command output to be written to file
381		waitForFile(t, outFile)
382
383		// Check file contents
384		outputContent, err := os.ReadFile(outFile)
385		if err != nil {
386			t.Fatalf("Failed to read output file: %v", err)
387		}
388		// The implementation appends a completion message to the output
389		outputStr := string(outputContent)
390		if !strings.Contains(outputStr, "Hello from background 1") {
391			t.Errorf("Expected output to contain 'Hello from background 1', got %q", outputStr)
392		}
393		if !strings.Contains(outputStr, "[background process completed]") {
394			t.Errorf("Expected output to contain completion message, got %q", outputStr)
395		}
396
397		// Clean up
398		os.Remove(outFile)
399		os.Remove(filepath.Dir(outFile))
400	})
401
402	// Test background command with stderr output
403	t.Run("Background Command with stderr", func(t *testing.T) {
404		inputObj := struct {
405			Command    string `json:"command"`
406			Background bool   `json:"background"`
407		}{
408			Command:    "echo 'Output to stdout' && echo 'Output to stderr' >&2",
409			Background: true,
410		}
411		inputJSON, err := json.Marshal(inputObj)
412		if err != nil {
413			t.Fatalf("Failed to marshal input: %v", err)
414		}
415
416		toolOut := tool.Run(context.Background(), inputJSON)
417		if toolOut.Error != nil {
418			t.Fatalf("Unexpected error: %v", toolOut.Error)
419		}
420		result := toolOut.LLMContent
421
422		// Parse the returned XML-ish format
423		resultStr := result[0].Text
424		lines := strings.Split(resultStr, "\n")
425		var outFile string
426		for _, line := range lines {
427			if strings.Contains(line, "<output_file>") {
428				start := strings.Index(line, "<output_file>") + len("<output_file>")
429				end := strings.Index(line, "</output_file>")
430				if end > start {
431					outFile = line[start:end]
432				}
433				break
434			}
435		}
436
437		// Wait for the command output to be written to file
438		waitForFile(t, outFile)
439
440		// Check output content (stdout and stderr are combined in implementation)
441		outputContent, err := os.ReadFile(outFile)
442		if err != nil {
443			t.Fatalf("Failed to read output file: %v", err)
444		}
445		// Implementation combines stdout and stderr into one file
446		outputStr := string(outputContent)
447		if !strings.Contains(outputStr, "Output to stdout") || !strings.Contains(outputStr, "Output to stderr") {
448			t.Errorf("Expected both stdout and stderr content, got %q", outputStr)
449		}
450
451		// Clean up
452		os.Remove(outFile)
453		os.Remove(filepath.Dir(outFile))
454	})
455
456	// Test background command running without waiting
457	t.Run("Background Command Running", func(t *testing.T) {
458		// Create a script that will continue running after we check
459		inputObj := struct {
460			Command    string `json:"command"`
461			Background bool   `json:"background"`
462		}{
463			Command:    "echo 'Running in background' && sleep 5",
464			Background: true,
465		}
466		inputJSON, err := json.Marshal(inputObj)
467		if err != nil {
468			t.Fatalf("Failed to marshal input: %v", err)
469		}
470
471		// Start the command in the background
472		toolOut := tool.Run(context.Background(), inputJSON)
473		if toolOut.Error != nil {
474			t.Fatalf("Unexpected error: %v", toolOut.Error)
475		}
476		result := toolOut.LLMContent
477
478		// Parse the returned XML-ish format
479		resultStr := result[0].Text
480		lines := strings.Split(resultStr, "\n")
481		var pidStr, outFile string
482		for _, line := range lines {
483			if strings.Contains(line, "<pid>") {
484				start := strings.Index(line, "<pid>") + len("<pid>")
485				end := strings.Index(line, "</pid>")
486				if end > start {
487					pidStr = line[start:end]
488				}
489			} else if strings.Contains(line, "<output_file>") {
490				start := strings.Index(line, "<output_file>") + len("<output_file>")
491				end := strings.Index(line, "</output_file>")
492				if end > start {
493					outFile = line[start:end]
494				}
495			}
496		}
497
498		// Wait for the command output to be written to file
499		waitForFile(t, outFile)
500
501		// Check output content
502		outputContent, err := os.ReadFile(outFile)
503		if err != nil {
504			t.Fatalf("Failed to read output file: %v", err)
505		}
506		expectedOutput := "Running in background\n"
507		if string(outputContent) != expectedOutput {
508			t.Errorf("Expected output content %q, got %q", expectedOutput, string(outputContent))
509		}
510
511		// Verify the process is still running by parsing PID
512		if pidStr != "" {
513			// We can't easily test if the process is still running without importing strconv
514			// and the process might have finished by now anyway due to timing
515			t.Log("Process started in background with PID:", pidStr)
516		}
517
518		// Clean up
519		os.Remove(outFile)
520		os.Remove(filepath.Dir(outFile))
521	})
522}
523
524func TestBashTimeout(t *testing.T) {
525	// Test default timeout values
526	t.Run("Default Timeout Values", func(t *testing.T) {
527		// Test foreground default timeout
528		foreground := bashInput{
529			Command:    "echo 'test'",
530			Background: false,
531		}
532		fgTimeout := foreground.timeout(nil)
533		expectedFg := 30 * time.Second
534		if fgTimeout != expectedFg {
535			t.Errorf("Expected foreground default timeout to be %v, got %v", expectedFg, fgTimeout)
536		}
537
538		// Test background default timeout
539		background := bashInput{
540			Command:    "echo 'test'",
541			Background: true,
542		}
543		bgTimeout := background.timeout(nil)
544		expectedBg := 24 * time.Hour
545		if bgTimeout != expectedBg {
546			t.Errorf("Expected background default timeout to be %v, got %v", expectedBg, bgTimeout)
547		}
548
549		// Test slow_ok timeout
550		slowOk := bashInput{
551			Command:    "echo 'test'",
552			Background: false,
553			SlowOK:     true,
554		}
555		slowTimeout := slowOk.timeout(nil)
556		expectedSlow := 15 * time.Minute
557		if slowTimeout != expectedSlow {
558			t.Errorf("Expected slow_ok timeout to be %v, got %v", expectedSlow, slowTimeout)
559		}
560
561		// Test custom timeout config
562		customTimeouts := &Timeouts{
563			Fast:       5 * time.Second,
564			Slow:       2 * time.Minute,
565			Background: 1 * time.Hour,
566		}
567		customFast := bashInput{
568			Command:    "echo 'test'",
569			Background: false,
570		}
571		customTimeout := customFast.timeout(customTimeouts)
572		expectedCustom := 5 * time.Second
573		if customTimeout != expectedCustom {
574			t.Errorf("Expected custom timeout to be %v, got %v", expectedCustom, customTimeout)
575		}
576	})
577}
578
579func TestFormatForegroundBashOutput(t *testing.T) {
580	// Test small output (under threshold) - should pass through unchanged
581	t.Run("Small Output", func(t *testing.T) {
582		smallOutput := "line 1\nline 2\nline 3\n"
583		result, err := formatForegroundBashOutput(smallOutput)
584		if err != nil {
585			t.Fatalf("Unexpected error: %v", err)
586		}
587		if result != smallOutput {
588			t.Errorf("Expected small output to pass through unchanged, got %q", result)
589		}
590	})
591
592	// Test large output (over 50KB) - should save to file and return summary
593	t.Run("Large Output With Lines", func(t *testing.T) {
594		// Generate output > 50KB with many lines
595		var lines []string
596		for i := 1; i <= 1000; i++ {
597			lines = append(lines, strings.Repeat("x", 60)+" line "+string(rune('0'+i%10)))
598		}
599		largeOutput := strings.Join(lines, "\n")
600		if len(largeOutput) < largeOutputThreshold {
601			t.Fatalf("Test setup error: output is only %d bytes, need > %d", len(largeOutput), largeOutputThreshold)
602		}
603
604		result, err := formatForegroundBashOutput(largeOutput)
605		if err != nil {
606			t.Fatalf("Unexpected error: %v", err)
607		}
608
609		// Should mention the file
610		if !strings.Contains(result, "saved to:") {
611			t.Errorf("Expected result to mention saved file, got:\n%s", result)
612		}
613
614		// Should have first 2 lines numbered
615		if !strings.Contains(result, "    1:") || !strings.Contains(result, "    2:") {
616			t.Errorf("Expected first 2 numbered lines, got:\n%s", result)
617		}
618
619		// Should have last 5 lines numbered
620		if !strings.Contains(result, "  996:") || !strings.Contains(result, " 1000:") {
621			t.Errorf("Expected last 5 numbered lines, got:\n%s", result)
622		}
623
624		t.Logf("Large output result:\n%s", result)
625	})
626
627	// Test large output with few/no lines (binary-like)
628	t.Run("Large Output No Lines", func(t *testing.T) {
629		// Generate > 50KB of data with no newlines
630		largeOutput := strings.Repeat("x", largeOutputThreshold+1000)
631
632		result, err := formatForegroundBashOutput(largeOutput)
633		if err != nil {
634			t.Fatalf("Unexpected error: %v", err)
635		}
636
637		// Should mention the file
638		if !strings.Contains(result, "saved to:") {
639			t.Errorf("Expected result to mention saved file, got:\n%s", result)
640		}
641
642		// Should indicate line count
643		if !strings.Contains(result, "1 lines") {
644			t.Errorf("Expected result to indicate line count, got:\n%s", result)
645		}
646
647		t.Logf("Large binary-like output result:\n%s", result)
648	})
649
650	// Test large output with very long lines (e.g., minified JS)
651	t.Run("Large Output With Long Lines", func(t *testing.T) {
652		// Generate output > 50KB with few very long lines
653		longLine := strings.Repeat("abcdefghij", 1000) // 10KB per line
654		lines := []string{longLine, longLine, longLine, longLine, longLine, longLine}
655		largeOutput := strings.Join(lines, "\n")
656		if len(largeOutput) < largeOutputThreshold {
657			t.Fatalf("Test setup error: output is only %d bytes, need > %d", len(largeOutput), largeOutputThreshold)
658		}
659
660		result, err := formatForegroundBashOutput(largeOutput)
661		if err != nil {
662			t.Fatalf("Unexpected error: %v", err)
663		}
664
665		// Result should be reasonable size (not blow up context)
666		if len(result) > 4096 {
667			t.Errorf("Expected truncated result < 4KB, got %d bytes:\n%s", len(result), result)
668		}
669
670		// Should mention the file
671		if !strings.Contains(result, "saved to:") {
672			t.Errorf("Expected result to mention saved file, got:\n%s", result)
673		}
674
675		// Lines should be truncated
676		if !strings.Contains(result, "...") {
677			t.Errorf("Expected truncated lines with '...', got:\n%s", result)
678		}
679
680		t.Logf("Large output with long lines result:\n%s", result)
681	})
682}
683
684// waitForFile waits for a file to exist and be non-empty or times out
685func waitForFile(t *testing.T, filepath string) {
686	timeout := time.After(5 * time.Second)
687	tick := time.NewTicker(10 * time.Millisecond)
688	defer tick.Stop()
689
690	for {
691		select {
692		case <-timeout:
693			t.Fatalf("Timed out waiting for file to exist and have contents: %s", filepath)
694			return
695		case <-tick.C:
696			info, err := os.Stat(filepath)
697			if err == nil && info.Size() > 0 {
698				return // File exists and has content
699			}
700		}
701	}
702}
703
704// waitForProcessDeath waits for a process to no longer exist or times out
705func waitForProcessDeath(t *testing.T, pid int) {
706	timeout := time.After(5 * time.Second)
707	tick := time.NewTicker(50 * time.Millisecond)
708	defer tick.Stop()
709
710	for {
711		select {
712		case <-timeout:
713			t.Fatalf("Timed out waiting for process %d to exit", pid)
714			return
715		case <-tick.C:
716			process, _ := os.FindProcess(pid)
717			err := process.Signal(syscall.Signal(0))
718			if err != nil {
719				// Process doesn't exist
720				return
721			}
722		}
723	}
724}
725
726func TestIsNoTrailerSet(t *testing.T) {
727	bashTool := &BashTool{WorkingDir: NewMutableWorkingDir("/")}
728
729	// Test when config is not set (default)
730	t.Run("Default No Config", func(t *testing.T) {
731		if bashTool.isNoTrailerSet() {
732			t.Error("Expected isNoTrailerSet() to be false when not configured")
733		}
734	})
735
736	// Test when config is set to true
737	t.Run("Config Set True", func(t *testing.T) {
738		// Set the global config
739		cmd := exec.Command("git", "config", "--global", "shelley.no-trailer", "true")
740		if err := cmd.Run(); err != nil {
741			t.Skipf("Could not set git config: %v", err)
742		}
743		defer exec.Command("git", "config", "--global", "--unset", "shelley.no-trailer").Run()
744
745		if !bashTool.isNoTrailerSet() {
746			t.Error("Expected isNoTrailerSet() to be true when shelley.no-trailer=true")
747		}
748	})
749
750	// Test when config is set to false
751	t.Run("Config Set False", func(t *testing.T) {
752		cmd := exec.Command("git", "config", "--global", "shelley.no-trailer", "false")
753		if err := cmd.Run(); err != nil {
754			t.Skipf("Could not set git config: %v", err)
755		}
756		defer exec.Command("git", "config", "--global", "--unset", "shelley.no-trailer").Run()
757
758		if bashTool.isNoTrailerSet() {
759			t.Error("Expected isNoTrailerSet() to be false when shelley.no-trailer=false")
760		}
761	})
762}