返回 DeepSeek-Reasonix
bash_heredoc_test.go
根目录 / internal / tool / builtin / bash_heredoc_test.go
1 package builtin
2
3 import (
4 "context"
5 "os"
6 "os/exec"
7 "path/filepath"
8 "strings"
9 "testing"
10 "time"
11
12 "reasonix/internal/sandbox"
13 )
14
15 func TestBashHereDocIssue5624CommandsReturnPromptly(t *testing.T) {
16 sh := requireHereDocBash(t)
17 prevBashShellPATH := bashShellPATH
18 bashShellPATH = func(context.Context) string { return "" }
19 t.Cleanup(func() { bashShellPATH = prevBashShellPATH })
20
21 root := t.TempDir()
22 if err := os.MkdirAll(filepath.Join(root, "app", "platform", "github"), 0o755); err != nil {
23 t.Fatalf("mkdir fixture: %v", err)
24 }
25
26 appendTarget := filepath.Join(root, "app", "platform", "github", "adapter.go")
27 redactTarget := filepath.Join(root, "test_redact.go")
28 tests := []struct {
29 name string
30 command string
31 target string
32 wantOut string
33 wantInFile []string
34 }{
35 {
36 name: "append pull request mapping heredoc",
37 command: strings.Join([]string{
38 "cat >> app/platform/github/adapter.go << 'EOF'",
39 "",
40 "// Pull request raw mapping (v0.6+).",
41 "type githubPullRaw struct {",
42 "\tNumber int `json:\"number\"`",
43 "\tTitle string `json:\"title\"`",
44 "}",
45 "",
46 "func githubPullToDetail(mergeable *bool) bool {",
47 "\treturn mergeable != nil && *mergeable",
48 "}",
49 "EOF",
50 "echo \"appended OK\"",
51 }, "\n"),
52 target: appendTarget,
53 wantOut: "appended OK",
54 wantInFile: []string{"type githubPullRaw struct", "`json:\"number\"`", "mergeable != nil && *mergeable"},
55 },
56 {
57 name: "cd then write redact repro heredoc",
58 command: strings.Join([]string{
59 "cd " + bashQuote(filepath.ToSlash(root)) + " && cat > " + bashQuote(filepath.ToSlash(redactTarget)) + " <<'EOF'",
60 "package main",
61 "",
62 "import (",
63 "\t\"encoding/json\"",
64 "\t\"fmt\"",
65 ")",
66 "",
67 "func main() {",
68 "\tdata := []byte(`{\"accounts\":[{\"id\":\"a1\",\"username\":\"alice\",\"token\":\"TOKEN_EXAMPLE\"}]}`)",
69 "\tvar v any",
70 "\tjson.Unmarshal(data, &v)",
71 "\tfmt.Printf(\"before: %v\\n\", v)",
72 "}",
73 "EOF",
74 "echo \"skip\"",
75 }, "\n"),
76 target: redactTarget,
77 wantOut: "skip",
78 wantInFile: []string{"package main", "TOKEN_EXAMPLE", "fmt.Printf(\"before: %v\\n\", v)"},
79 },
80 }
81
82 for _, tt := range tests {
83 t.Run(tt.name, func(t *testing.T) {
84 ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
85 defer cancel()
86 ctx = fullAccessBashTestContext(ctx)
87
88 done := make(chan struct {
89 out string
90 err error
91 elapsed time.Duration
92 }, 1)
93 go func() {
94 start := time.Now()
95 out, err := (bash{shell: sh, workDir: root, timeout: 3 * time.Second}).Execute(ctx, argsJSON(t, map[string]any{"command": tt.command}))
96 done <- struct {
97 out string
98 err error
99 elapsed time.Duration
100 }{out: out, err: err, elapsed: time.Since(start)}
101 }()
102
103 var got struct {
104 out string
105 err error
106 elapsed time.Duration
107 }
108 select {
109 case got = <-done:
110 case <-time.After(10 * time.Second):
111 t.Fatal("heredoc bash command did not return within 10s")
112 }
113 if got.err != nil {
114 t.Fatalf("bash heredoc failed after %v: %v (out=%q)", got.elapsed, got.err, got.out)
115 }
116 if got.elapsed > 2*time.Second {
117 t.Fatalf("bash heredoc returned too slowly: %v (out=%q)", got.elapsed, got.out)
118 }
119 if !strings.Contains(got.out, tt.wantOut) {
120 t.Fatalf("output = %q, want %q", got.out, tt.wantOut)
121 }
122 data, err := os.ReadFile(tt.target)
123 if err != nil {
124 t.Fatalf("read heredoc target: %v", err)
125 }
126 body := string(data)
127 for _, want := range tt.wantInFile {
128 if !strings.Contains(body, want) {
129 t.Fatalf("target missing %q:\n%s", want, body)
130 }
131 }
132 })
133 }
134 }
135
136 func requireHereDocBash(t *testing.T) sandbox.Shell {
137 t.Helper()
138 sh := sandbox.ResolveShell("bash", "", nil)
139 if sh.Kind != sandbox.ShellBash {
140 t.Skipf("bash heredoc regression requires bash, got %s", sh.Kind.String())
141 }
142 path := sh.Path
143 if path == "" {
144 path = "bash"
145 }
146 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
147 defer cancel()
148 if err := exec.CommandContext(ctx, path, "-c", "true").Run(); err != nil {
149 t.Skipf("bash heredoc regression requires a runnable bash: %v", err)
150 }
151 sh.Path = path
152 return sh
153 }
154
155 func bashQuote(s string) string {
156 return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'"
157 }
158
158 lines GO