返回 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 go func() { time.Sleep(300 * time.Millisecond); cancel() }()
28
29 start := time.Now()
30 done := make(chan error, 1)
31 go func() {
32 _, err := bt.Execute(ctx, args)
33 done <- err
34 }()
35
36 // The kill must land well before the 120s natural duration; the generous
37 // watchdog only trips when the cancel path is actually broken, so a loaded
38 // machine's slow process-tree teardown doesn't flake the test.
39 var err error
40 select {
41 case err = <-done:
42 case <-time.After(40 * time.Second):
43 t.Fatalf("cancel did not interrupt bash within 40s (natural duration 120s)")
44 }
45 elapsed := time.Since(start)
46
47 // Must have run until the cancel (≥ ~300ms) — not failed instantly.
48 if elapsed < 250*time.Millisecond {
49 t.Fatalf("command exited too fast (%v) — it didn't actually run; err=%v", elapsed, err)
50 }
51 if err == nil {
52 t.Error("expected an error after cancel, got nil")
53 }
54 t.Logf("cancelled bash (%q) returned in %v (err=%v)", cmd, elapsed, err)
55 }
56
56 lines GO