返回 DeepSeek-Reasonix
bash_timeout_test.go
根目录 / internal / tool / builtin / bash_timeout_test.go
1 package builtin
2
3 import (
4 "context"
5 "errors"
6 "os/exec"
7 "strings"
8 "testing"
9 "time"
10
11 "reasonix/internal/sandbox"
12 "reasonix/internal/tool"
13 )
14
15 func TestBashForegroundTimeoutConfig(t *testing.T) {
16 sh := sandbox.ResolveShell("", "", nil)
17 b := bash{shell: sh, timeout: 150 * time.Millisecond}
18
19 start := time.Now()
20 out, err := b.Execute(fullAccessBashTestContext(t.Context()), argsJSON(t, map[string]any{"command": longSleepCommand(sh)}))
21 elapsed := time.Since(start)
22 if err == nil {
23 t.Fatalf("expected timeout error, got nil (out=%q)", out)
24 }
25 if !strings.Contains(err.Error(), "timed out") {
26 t.Fatalf("error = %v, want timeout", err)
27 }
28 if elapsed > 5*time.Second {
29 t.Fatalf("configured timeout returned too slowly: %v", elapsed)
30 }
31 }
32
33 func TestBashExplicitZeroTimeoutDoesNotCapForeground(t *testing.T) {
34 sh := sandbox.ResolveShell("", "", nil)
35 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
36 defer cancel()
37 ctx = fullAccessBashTestContext(ctx)
38
39 start := time.Now()
40 out, err := (bash{shell: sh, timeout: 0}).Execute(ctx, argsJSON(t, map[string]any{"command": oneSecondCommand(sh)}))
41 elapsed := time.Since(start)
42 if err != nil {
43 t.Fatalf("zero-timeout foreground command failed: %v (out=%q)", err, out)
44 }
45 if !strings.Contains(out, "done") {
46 t.Fatalf("output = %q, want done", out)
47 }
48 if elapsed < 800*time.Millisecond {
49 t.Fatalf("command returned too quickly (%v), so the sleep did not run", elapsed)
50 }
51 }
52
53 func TestWorkspacePassesBashTimeout(t *testing.T) {
54 sh := sandbox.ResolveShell("", "", nil)
55 var shells []tool.Tool
56 for _, candidate := range (Workspace{Dir: t.TempDir(), BashTimeout: 150 * time.Millisecond}).Tools() {
57 if tool.IsShellToolName(candidate.Name()) {
58 shells = append(shells, candidate)
59 }
60 }
61 if len(shells) != 1 {
62 t.Fatalf("workspace shell tools = %v, want exactly one primary shell", toolNames(shells))
63 }
64 wantName := "bash"
65 if sh.Kind == sandbox.ShellPowerShell {
66 wantName = "pwsh"
67 }
68 if shells[0].Name() != wantName {
69 t.Fatalf("workspace shell name = %q, want %q", shells[0].Name(), wantName)
70 }
71
72 out, err := shells[0].Execute(fullAccessBashTestContext(t.Context()), argsJSON(t, map[string]any{"command": longSleepCommand(sh), "description": "exercise configured shell timeout"}))
73 if err == nil {
74 t.Fatalf("expected workspace bash timeout, got nil (out=%q)", out)
75 }
76 if !strings.Contains(err.Error(), "timed out") {
77 t.Fatalf("error = %v, want timeout", err)
78 }
79 }
80
81 func toolNames(tools []tool.Tool) []string {
82 names := make([]string, 0, len(tools))
83 for _, candidate := range tools {
84 names = append(names, candidate.Name())
85 }
86 return names
87 }
88
89 func TestNormalizeBashRunErrorAllowsPreservedWaitDelay(t *testing.T) {
90 if err := normalizeBashRunError(context.Background(), exec.ErrWaitDelay, true); err != nil {
91 t.Fatalf("preserved post-exit WaitDelay should be ignored, got %v", err)
92 }
93 if err := normalizeBashRunError(context.Background(), exec.ErrWaitDelay, false); !errors.Is(err, exec.ErrWaitDelay) {
94 t.Fatalf("ordinary WaitDelay should remain visible, got %v", err)
95 }
96
97 ctx, cancel := context.WithCancel(context.Background())
98 cancel()
99 if err := normalizeBashRunError(ctx, exec.ErrWaitDelay, true); !errors.Is(err, exec.ErrWaitDelay) {
100 t.Fatalf("cancelled WaitDelay should remain visible, got %v", err)
101 }
102 }
103
104 func longSleepCommand(sh sandbox.Shell) string {
105 if sh.Kind == sandbox.ShellPowerShell {
106 return "Start-Sleep -Seconds 2"
107 }
108 return "sleep 2"
109 }
110
111 func oneSecondCommand(sh sandbox.Shell) string {
112 if sh.Kind == sandbox.ShellPowerShell {
113 return "Start-Sleep -Seconds 1; Write-Output done"
114 }
115 return "sleep 1; printf done"
116 }
117
118 func BenchmarkBashForegroundTimeoutExplicitZero(b *testing.B) {
119 bt := bash{timeout: 0}
120 ctx := context.Background()
121 for b.Loop() {
122 runCtx := ctx
123 timeout := bt.foregroundTimeout()
124 if timeout > 0 {
125 b.Fatal("zero-value bash should not create a timeout context")
126 }
127 if runCtx == nil {
128 b.Fatal("nil context")
129 }
130 }
131 }
132
133 func BenchmarkBashForegroundTimeoutConfiguredCap(b *testing.B) {
134 bt := bash{timeout: 120 * time.Second}
135 ctx := context.Background()
136 for b.Loop() {
137 runCtx := ctx
138 timeout := bt.foregroundTimeout()
139 if timeout > 0 {
140 var cancel context.CancelFunc
141 runCtx, cancel = context.WithTimeoutCause(ctx, timeout, errors.New("bash foreground timeout"))
142 cancel()
143 }
144 if runCtx == nil {
145 b.Fatal("nil context")
146 }
147 }
148 }
149
149 lines GO