1package tools
2
3import (
4 "context"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/charmbracelet/crush/internal/shell"
10 "github.com/stretchr/testify/require"
11)
12
13func TestBackgroundShell_Integration(t *testing.T) {
14 t.Parallel()
15
16 workingDir := t.TempDir()
17 ctx := context.Background()
18
19 // Start a background shell
20 bgManager := shell.GetBackgroundShellManager()
21 bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'hello background' && echo 'done'", "")
22 require.NoError(t, err)
23 require.NotEmpty(t, bgShell.ID)
24
25 // Wait for completion
26 bgShell.Wait()
27
28 // Check final output
29 stdout, stderr, done, err := bgShell.GetOutput()
30 require.NoError(t, err)
31 require.Contains(t, stdout, "hello background")
32 require.Contains(t, stdout, "done")
33 require.True(t, done)
34 require.Empty(t, stderr)
35
36 // Clean up
37 bgManager.Kill(bgShell.ID)
38}
39
40func TestBackgroundShell_Kill(t *testing.T) {
41 t.Parallel()
42
43 workingDir := t.TempDir()
44 ctx := context.Background()
45
46 // Start a long-running background shell
47 bgManager := shell.GetBackgroundShellManager()
48 bgShell, err := bgManager.Start(ctx, workingDir, nil, "sleep 100", "")
49 require.NoError(t, err)
50
51 // Kill it
52 err = bgManager.Kill(bgShell.ID)
53 require.NoError(t, err)
54
55 // Verify it's gone
56 _, ok := bgManager.Get(bgShell.ID)
57 require.False(t, ok)
58
59 // Verify the shell is done
60 require.True(t, bgShell.IsDone())
61}
62
63func TestBackgroundShell_GetOutput_NoHang(t *testing.T) {
64 t.Parallel()
65
66 workingDir := t.TempDir()
67 ctx := context.Background()
68
69 // Start a long-running background shell
70 bgManager := shell.GetBackgroundShellManager()
71 bgShell, err := bgManager.Start(ctx, workingDir, nil, "while true; do echo \"Hello from background job - $(date +%T)\"; sleep 1; done", "")
72 require.NoError(t, err)
73 defer bgManager.Kill(bgShell.ID)
74 // wait for 2 seconds
75 time.Sleep(2 * time.Second)
76 stdout, _, _, err := bgShell.GetOutput()
77 require.NoError(t, err)
78 require.Len(t, strings.Split(stdout, "\n"), 3)
79}
80
81func TestBackgroundShell_MultipleOutputCalls(t *testing.T) {
82 t.Parallel()
83
84 workingDir := t.TempDir()
85 ctx := context.Background()
86
87 // Start a background shell
88 bgManager := shell.GetBackgroundShellManager()
89 bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'step 1' && echo 'step 2' && echo 'step 3'", "")
90 require.NoError(t, err)
91 defer bgManager.Kill(bgShell.ID)
92
93 // Check that we can call GetOutput multiple times while running
94 for range 5 {
95 _, _, done, _ := bgShell.GetOutput()
96 if done {
97 break
98 }
99 time.Sleep(10 * time.Millisecond)
100 }
101
102 // Wait for completion
103 bgShell.Wait()
104
105 // Multiple calls after completion should return the same result
106 stdout1, _, done1, _ := bgShell.GetOutput()
107 require.True(t, done1)
108 require.Contains(t, stdout1, "step 1")
109 require.Contains(t, stdout1, "step 2")
110 require.Contains(t, stdout1, "step 3")
111
112 stdout2, _, done2, _ := bgShell.GetOutput()
113 require.True(t, done2)
114 require.Equal(t, stdout1, stdout2, "Multiple GetOutput calls should return same result")
115}
116
117func TestBackgroundShell_EmptyOutput(t *testing.T) {
118 t.Parallel()
119
120 workingDir := t.TempDir()
121 ctx := context.Background()
122
123 // Start a background shell with no output
124 bgManager := shell.GetBackgroundShellManager()
125 bgShell, err := bgManager.Start(ctx, workingDir, nil, "sleep 0.1", "")
126 require.NoError(t, err)
127 defer bgManager.Kill(bgShell.ID)
128
129 // Wait for completion
130 bgShell.Wait()
131
132 stdout, stderr, done, err := bgShell.GetOutput()
133 require.NoError(t, err)
134 require.Empty(t, stdout)
135 require.Empty(t, stderr)
136 require.True(t, done)
137}
138
139func TestBackgroundShell_ExitCode(t *testing.T) {
140 t.Parallel()
141
142 workingDir := t.TempDir()
143 ctx := context.Background()
144
145 // Start a background shell that exits with non-zero code
146 bgManager := shell.GetBackgroundShellManager()
147 bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'failing' && exit 42", "")
148 require.NoError(t, err)
149 defer bgManager.Kill(bgShell.ID)
150
151 // Wait for completion
152 bgShell.Wait()
153
154 stdout, _, done, execErr := bgShell.GetOutput()
155 require.True(t, done)
156 require.Contains(t, stdout, "failing")
157 require.Error(t, execErr)
158
159 exitCode := shell.ExitCode(execErr)
160 require.Equal(t, 42, exitCode)
161}
162
163func TestBackgroundShell_WithBlockFuncs(t *testing.T) {
164 t.Parallel()
165
166 workingDir := t.TempDir()
167 ctx := context.Background()
168
169 blockFuncs := []shell.BlockFunc{
170 shell.CommandsBlocker([]string{"curl", "wget"}),
171 }
172
173 // Start a background shell with a blocked command
174 bgManager := shell.GetBackgroundShellManager()
175 bgShell, err := bgManager.Start(ctx, workingDir, blockFuncs, "curl example.com", "")
176 require.NoError(t, err)
177 defer bgManager.Kill(bgShell.ID)
178
179 // Wait for completion
180 bgShell.Wait()
181
182 stdout, stderr, done, execErr := bgShell.GetOutput()
183 require.True(t, done)
184
185 // The command should have been blocked, check stderr or error
186 if execErr != nil {
187 // Error might contain the message
188 require.Contains(t, execErr.Error(), "not allowed")
189 } else {
190 // Or it might be in stderr
191 output := stdout + stderr
192 require.Contains(t, output, "not allowed")
193 }
194}
195
196func TestBackgroundShell_StdoutAndStderr(t *testing.T) {
197 t.Parallel()
198
199 workingDir := t.TempDir()
200 ctx := context.Background()
201
202 // Start a background shell with both stdout and stderr
203 bgManager := shell.GetBackgroundShellManager()
204 bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'stdout message' && echo 'stderr message' >&2", "")
205 require.NoError(t, err)
206 defer bgManager.Kill(bgShell.ID)
207
208 // Wait for completion
209 bgShell.Wait()
210
211 stdout, stderr, done, err := bgShell.GetOutput()
212 require.NoError(t, err)
213 require.True(t, done)
214 require.Contains(t, stdout, "stdout message")
215 require.Contains(t, stderr, "stderr message")
216}
217
218func TestBackgroundShell_ConcurrentAccess(t *testing.T) {
219 t.Parallel()
220
221 workingDir := t.TempDir()
222 ctx := context.Background()
223
224 // Start a background shell
225 bgManager := shell.GetBackgroundShellManager()
226 bgShell, err := bgManager.Start(ctx, workingDir, nil, "for i in 1 2 3 4 5; do echo \"line $i\"; sleep 0.05; done", "")
227 require.NoError(t, err)
228 defer bgManager.Kill(bgShell.ID)
229
230 // Access output concurrently from multiple goroutines
231 done := make(chan struct{})
232 errors := make(chan error, 10)
233
234 for range 10 {
235 go func() {
236 for {
237 select {
238 case <-done:
239 return
240 default:
241 _, _, _, err := bgShell.GetOutput()
242 if err != nil {
243 errors <- err
244 }
245 dir := bgShell.WorkingDir
246 if dir == "" {
247 errors <- err
248 }
249 time.Sleep(10 * time.Millisecond)
250 }
251 }
252 }()
253 }
254
255 // Let it run for a bit
256 time.Sleep(300 * time.Millisecond)
257 close(done)
258
259 // Check for any errors
260 select {
261 case err := <-errors:
262 t.Fatalf("Concurrent access caused error: %v", err)
263 case <-time.After(100 * time.Millisecond):
264 // No errors - success
265 }
266}
267
268func TestBackgroundShell_List(t *testing.T) {
269 t.Parallel()
270
271 workingDir := t.TempDir()
272 ctx := context.Background()
273
274 bgManager := shell.GetBackgroundShellManager()
275
276 // Start multiple background shells
277 shells := make([]*shell.BackgroundShell, 3)
278 for i := range 3 {
279 bgShell, err := bgManager.Start(ctx, workingDir, nil, "sleep 1", "")
280 require.NoError(t, err)
281 shells[i] = bgShell
282 }
283
284 // Get the list
285 ids := bgManager.List()
286
287 // Verify all our shells are in the list
288 for _, sh := range shells {
289 require.Contains(t, ids, sh.ID, "Shell %s not found in list", sh.ID)
290 }
291
292 // Clean up
293 for _, sh := range shells {
294 bgManager.Kill(sh.ID)
295 }
296}
297
298func TestBackgroundShell_AutoBackground(t *testing.T) {
299 t.Parallel()
300
301 workingDir := t.TempDir()
302 ctx := context.Background()
303
304 // Test that a quick command completes synchronously
305 t.Run("quick command completes synchronously", func(t *testing.T) {
306 t.Parallel()
307 bgManager := shell.GetBackgroundShellManager()
308 bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'quick'", "")
309 require.NoError(t, err)
310
311 // Wait threshold time
312 time.Sleep(5 * time.Second)
313
314 // Should be done by now
315 stdout, stderr, done, err := bgShell.GetOutput()
316 require.NoError(t, err)
317 require.True(t, done, "Quick command should be done")
318 require.Contains(t, stdout, "quick")
319 require.Empty(t, stderr)
320
321 // Clean up
322 bgManager.Kill(bgShell.ID)
323 })
324
325 // Test that a long command stays in background
326 t.Run("long command stays in background", func(t *testing.T) {
327 t.Parallel()
328 bgManager := shell.GetBackgroundShellManager()
329 bgShell, err := bgManager.Start(ctx, workingDir, nil, "sleep 20 && echo '20 seconds completed'", "")
330 require.NoError(t, err)
331 defer bgManager.Kill(bgShell.ID)
332
333 // Wait threshold time
334 time.Sleep(5 * time.Second)
335
336 // Should still be running
337 stdout, stderr, done, err := bgShell.GetOutput()
338 require.NoError(t, err)
339 require.False(t, done, "Long command should still be running")
340 require.Empty(t, stdout, "No output yet from sleep command")
341 require.Empty(t, stderr)
342
343 // Verify we can get the shell from manager
344 retrieved, ok := bgManager.Get(bgShell.ID)
345 require.True(t, ok, "Should be able to retrieve background shell")
346 require.Equal(t, bgShell.ID, retrieved.ID)
347 })
348
349 // Test that we can check output of long-running command later
350 t.Run("can check output after completion", func(t *testing.T) {
351 t.Parallel()
352 bgManager := shell.GetBackgroundShellManager()
353 bgShell, err := bgManager.Start(ctx, workingDir, nil, "sleep 3 && echo 'completed'", "")
354 require.NoError(t, err)
355 defer bgManager.Kill(bgShell.ID)
356
357 // Initially should be running
358 _, _, done, _ := bgShell.GetOutput()
359 require.False(t, done, "Should be running initially")
360
361 // Wait for completion
362 time.Sleep(4 * time.Second)
363
364 // Now should be done
365 stdout, stderr, done, err := bgShell.GetOutput()
366 require.NoError(t, err)
367 require.True(t, done, "Should be done after waiting")
368 require.Contains(t, stdout, "completed")
369 require.Empty(t, stderr)
370 })
371}