package builtin import ( "context" "errors" "os/exec" "strings" "testing" "time" "reasonix/internal/sandbox" ) func TestBashForegroundTimeoutConfig(t *testing.T) { sh := sandbox.ResolveShell("", "", nil) b := bash{shell: sh, timeout: 150 * time.Millisecond} start := time.Now() out, err := b.Execute(context.Background(), argsJSON(t, map[string]any{"command": longSleepCommand(sh)})) elapsed := time.Since(start) if err == nil { t.Fatalf("expected timeout error, got nil (out=%q)", out) } if !strings.Contains(err.Error(), "timed out") { t.Fatalf("error = %v, want timeout", err) } if elapsed > 5*time.Second { t.Fatalf("configured timeout returned too slowly: %v", elapsed) } } func TestBashExplicitZeroTimeoutDoesNotCapForeground(t *testing.T) { sh := sandbox.ResolveShell("", "", nil) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() start := time.Now() out, err := (bash{shell: sh, timeout: 0}).Execute(ctx, argsJSON(t, map[string]any{"command": oneSecondCommand(sh)})) elapsed := time.Since(start) if err != nil { t.Fatalf("zero-timeout foreground command failed: %v (out=%q)", err, out) } if !strings.Contains(out, "done") { t.Fatalf("output = %q, want done", out) } if elapsed < 800*time.Millisecond { t.Fatalf("command returned too quickly (%v), so the sleep did not run", elapsed) } } func TestWorkspacePassesBashTimeout(t *testing.T) { sh := sandbox.ResolveShell("", "", nil) b := byName(Workspace{Dir: t.TempDir(), BashTimeout: 150 * time.Millisecond}.Tools())["bash"] out, err := b.Execute(context.Background(), argsJSON(t, map[string]any{"command": longSleepCommand(sh)})) if err == nil { t.Fatalf("expected workspace bash timeout, got nil (out=%q)", out) } if !strings.Contains(err.Error(), "timed out") { t.Fatalf("error = %v, want timeout", err) } } func TestNormalizeBashRunErrorAllowsPreservedWaitDelay(t *testing.T) { if err := normalizeBashRunError(context.Background(), exec.ErrWaitDelay, true); err != nil { t.Fatalf("preserved post-exit WaitDelay should be ignored, got %v", err) } if err := normalizeBashRunError(context.Background(), exec.ErrWaitDelay, false); !errors.Is(err, exec.ErrWaitDelay) { t.Fatalf("ordinary WaitDelay should remain visible, got %v", err) } ctx, cancel := context.WithCancel(context.Background()) cancel() if err := normalizeBashRunError(ctx, exec.ErrWaitDelay, true); !errors.Is(err, exec.ErrWaitDelay) { t.Fatalf("cancelled WaitDelay should remain visible, got %v", err) } } func longSleepCommand(sh sandbox.Shell) string { if sh.Kind == sandbox.ShellPowerShell { return "Start-Sleep -Seconds 2" } return "sleep 2" } func oneSecondCommand(sh sandbox.Shell) string { if sh.Kind == sandbox.ShellPowerShell { return "Start-Sleep -Seconds 1; Write-Output done" } return "sleep 1; printf done" } func BenchmarkBashForegroundTimeoutExplicitZero(b *testing.B) { bt := bash{timeout: 0} ctx := context.Background() for b.Loop() { runCtx := ctx timeout := bt.foregroundTimeout() if timeout > 0 { b.Fatal("zero-value bash should not create a timeout context") } if runCtx == nil { b.Fatal("nil context") } } } func BenchmarkBashForegroundTimeoutConfiguredCap(b *testing.B) { bt := bash{timeout: 120 * time.Second} ctx := context.Background() for b.Loop() { runCtx := ctx timeout := bt.foregroundTimeout() if timeout > 0 { var cancel context.CancelFunc runCtx, cancel = context.WithTimeoutCause(ctx, timeout, errBashTimeout) cancel() } if runCtx == nil { b.Fatal("nil context") } } }