返回 DeepSeek-Reasonix
bash_cancel_test.go
根目录 / internal / tool / builtin / bash_cancel_test.go
1 package builtin
2
3 import (
4 "context"
5 "encoding/json"
6 "testing"
7 "time"
8
9 "reasonix/internal/sandbox"
10 "reasonix/internal/tool"
11 )
12
13 // TestBashCancelReturnsPromptly proves a cancelled bash run stops fast instead of
14 // blocking for the command's natural duration — the process-tree kill path.
15 func TestBashCancelReturnsPromptly(t *testing.T) {
16 bt, ok := tool.LookupBuiltin("bash")
17 if !ok {
18 t.Fatal("bash not registered")
19 }
20 cmd := "sleep 120"
21 if sandbox.ResolveShell("", "", nil).Kind == sandbox.ShellPowerShell {
22 cmd = "Start-Sleep -Seconds 120"
23 }
24 args, _ := json.Marshal(map[string]any{"command": cmd})
25
26 ctx, cancel := context.WithCancel(context.Background())
27 // This fixture tests process cancellation, independently of sandbox availability.
28 ctx = sandbox.WithPermissionPreset(ctx, "danger-full-access")
29 defer cancel()
30 go func() { time.Sleep(300 * time.Millisecond); cancel() }()
31
32 start := time.Now()
33 done := make(chan error, 1)
34 go func() {
35 _, err := bt.Execute(ctx, args)
36 done <- err
37 }()
38
39 // The kill must land well before the 120s natural duration; the generous
40 // watchdog only trips when the cancel path is actually broken, so a loaded
41 // machine's slow process-tree teardown doesn't flake the test.
42 var err error
43 select {
44 case err = <-done:
45 case <-time.After(40 * time.Second):
46 t.Fatalf("cancel did not interrupt bash within 40s (natural duration 120s)")
47 }
48 elapsed := time.Since(start)
49
50 // Must have run until the cancel (≥ ~300ms) — not failed instantly.
51 if elapsed < 250*time.Millisecond {
52 t.Fatalf("command exited too fast (%v) — it didn't actually run; err=%v", elapsed, err)
53 }
54 if err == nil {
55 t.Error("expected an error after cancel, got nil")
56 }
57 t.Logf("cancelled bash (%q) returned in %v (err=%v)", cmd, elapsed, err)
58 }
59
59 lines GO