返回 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
87 done := make(chan struct {
88 out string
89 err error
90 elapsed time.Duration
91 }, 1)
92 go func() {
93 start := time.Now()
94 out, err := (bash{shell: sh, workDir: root, timeout: 3 * time.Second}).Execute(ctx, argsJSON(t, map[string]any{"command": tt.command}))
95 done <- struct {
96 out string
97 err error
98 elapsed time.Duration
99 }{out: out, err: err, elapsed: time.Since(start)}
100 }()
101
102 var got struct {
103 out string
104 err error
105 elapsed time.Duration
106 }
107 select {
108 case got = <-done:
109 case <-time.After(10 * time.Second):
110 t.Fatal("heredoc bash command did not return within 10s")
111 }
112 if got.err != nil {
113 t.Fatalf("bash heredoc failed after %v: %v (out=%q)", got.elapsed, got.err, got.out)
114 }
115 if got.elapsed > 2*time.Second {
116 t.Fatalf("bash heredoc returned too slowly: %v (out=%q)", got.elapsed, got.out)
117 }
118 if !strings.Contains(got.out, tt.wantOut) {
119 t.Fatalf("output = %q, want %q", got.out, tt.wantOut)
120 }
121 data, err := os.ReadFile(tt.target)
122 if err != nil {
123 t.Fatalf("read heredoc target: %v", err)
124 }
125 body := string(data)
126 for _, want := range tt.wantInFile {
127 if !strings.Contains(body, want) {
128 t.Fatalf("target missing %q:\n%s", want, body)
129 }
130 }
131 })
132 }
133 }
134
135 func requireHereDocBash(t *testing.T) sandbox.Shell {
136 t.Helper()
137 sh := sandbox.ResolveShell("bash", "", nil)
138 if sh.Kind != sandbox.ShellBash {
139 t.Skipf("bash heredoc regression requires bash, got %s", sh.Kind.String())
140 }
141 path := sh.Path
142 if path == "" {
143 path = "bash"
144 }
145 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
146 defer cancel()
147 if err := exec.CommandContext(ctx, path, "-c", "true").Run(); err != nil {
148 t.Skipf("bash heredoc regression requires a runnable bash: %v", err)
149 }
150 sh.Path = path
151 return sh
152 }
153
154 func bashQuote(s string) string {
155 return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'"
156 }
157
157 lines GO