|
18 | 18 | package runtime |
19 | 19 |
|
20 | 20 | import ( |
| 21 | + "fmt" |
| 22 | + "os" |
21 | 23 | "strings" |
22 | 24 | "testing" |
23 | 25 | "time" |
@@ -173,6 +175,104 @@ func TestBashSessionEnvLargeOutputChained(t *testing.T) { |
173 | 175 | } |
174 | 176 | } |
175 | 177 |
|
| 178 | +func TestBashSession_heredoc(t *testing.T) { |
| 179 | + rewardDir := t.TempDir() |
| 180 | + controller := NewController("", "") |
| 181 | + |
| 182 | + hooks := ExecuteResultHook{ |
| 183 | + OnExecuteStdout: func(line string) { |
| 184 | + fmt.Printf("[stdout] %s\n", line) |
| 185 | + }, |
| 186 | + OnExecuteComplete: func(d time.Duration) { |
| 187 | + fmt.Printf("[complete] %s\n", d) |
| 188 | + }, |
| 189 | + } |
| 190 | + |
| 191 | + // First run: heredoc + reward file write. |
| 192 | + script := fmt.Sprintf(` |
| 193 | +set -x |
| 194 | +reward_dir=%q |
| 195 | +mkdir -p "$reward_dir" |
| 196 | +
|
| 197 | +cat > /tmp/repro_script.sh <<'SHEOF' |
| 198 | +#!/usr/bin/env sh |
| 199 | +echo "hello heredoc" |
| 200 | +SHEOF |
| 201 | +
|
| 202 | +chmod +x /tmp/repro_script.sh |
| 203 | +/tmp/repro_script.sh |
| 204 | +echo "after heredoc" |
| 205 | +echo 1 > "$reward_dir/reward.txt" |
| 206 | +cat "$reward_dir/reward.txt" |
| 207 | +`, rewardDir) |
| 208 | + |
| 209 | + if err := controller.Execute(&ExecuteCodeRequest{ |
| 210 | + Language: Bash, |
| 211 | + Timeout: 10 * time.Second, |
| 212 | + Code: script, |
| 213 | + Hooks: hooks, |
| 214 | + }); err != nil { |
| 215 | + fmt.Fprintf(os.Stderr, "first Execute failed: %v\n", err) |
| 216 | + os.Exit(1) |
| 217 | + } |
| 218 | + |
| 219 | + // Second run: ensure the session keeps working. |
| 220 | + if err := controller.Execute(&ExecuteCodeRequest{ |
| 221 | + Language: Bash, |
| 222 | + Timeout: 5 * time.Second, |
| 223 | + Code: "echo 'second command works'", |
| 224 | + Hooks: hooks, |
| 225 | + }); err != nil { |
| 226 | + fmt.Fprintf(os.Stderr, "second Execute failed: %v\n", err) |
| 227 | + os.Exit(1) |
| 228 | + } |
| 229 | +} |
| 230 | + |
| 231 | +func TestBashSession_execReplacesShell(t *testing.T) { |
| 232 | + session := newBashSession(nil) |
| 233 | + t.Cleanup(func() { _ = session.close() }) |
| 234 | + |
| 235 | + if err := session.start(); err != nil { |
| 236 | + t.Fatalf("Start() error = %v", err) |
| 237 | + } |
| 238 | + |
| 239 | + var stdoutLines []string |
| 240 | + hooks := ExecuteResultHook{ |
| 241 | + OnExecuteStdout: func(line string) { |
| 242 | + stdoutLines = append(stdoutLines, line) |
| 243 | + }, |
| 244 | + } |
| 245 | + |
| 246 | + script := ` |
| 247 | +cat > /tmp/exec_child.sh <<'EOF' |
| 248 | +echo "child says hi" |
| 249 | +EOF |
| 250 | +chmod +x /tmp/exec_child.sh |
| 251 | +exec /tmp/exec_child.sh |
| 252 | +` |
| 253 | + |
| 254 | + err := session.run(script, 5*time.Second, &hooks) |
| 255 | + if err == nil { |
| 256 | + t.Fatalf("expected error because exec replaces the shell, got nil") |
| 257 | + } |
| 258 | + if !strings.Contains(err.Error(), "stdout closed") && !strings.Contains(err.Error(), "terminated") { |
| 259 | + t.Fatalf("unexpected error for exec: %v", err) |
| 260 | + } |
| 261 | + if !containsLine(stdoutLines, "child says hi") { |
| 262 | + t.Fatalf("expected child output, got %v", stdoutLines) |
| 263 | + } |
| 264 | + if !session.terminated.Load() { |
| 265 | + t.Fatalf("expected session to be marked terminated after exec") |
| 266 | + } |
| 267 | + |
| 268 | + // Subsequent run should fail immediately because the shell was replaced. |
| 269 | + if err := session.run("echo still-alive", 2*time.Second, &hooks); err == nil { |
| 270 | + t.Fatalf("expected run to fail after exec replaced the shell") |
| 271 | + } else if !strings.Contains(err.Error(), "terminated") { |
| 272 | + t.Fatalf("expected terminated error, got %v", err) |
| 273 | + } |
| 274 | +} |
| 275 | + |
176 | 276 | func containsLine(lines []string, target string) bool { |
177 | 277 | for _, l := range lines { |
178 | 278 | if strings.TrimSpace(l) == target { |
|
0 commit comments