1package tools
2
3import (
4 "context"
5 "runtime"
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_MultipleOutputCalls(t *testing.T) {
64 t.Parallel()
65
66 workingDir := t.TempDir()
67 ctx := context.Background()
68
69 // Start a background shell
70 bgManager := shell.GetBackgroundShellManager()
71 bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'step 1' && echo 'step 2' && echo 'step 3'", "")
72 require.NoError(t, err)
73 defer bgManager.Kill(bgShell.ID)
74
75 // Check that we can call GetOutput multiple times while running
76 for range 5 {
77 _, _, done, _ := bgShell.GetOutput()
78 if done {
79 break
80 }
81 time.Sleep(10 * time.Millisecond)
82 }
83
84 // Wait for completion
85 bgShell.Wait()
86
87 // Multiple calls after completion should return the same result
88 stdout1, _, done1, _ := bgShell.GetOutput()
89 require.True(t, done1)
90 require.Contains(t, stdout1, "step 1")
91 require.Contains(t, stdout1, "step 2")
92 require.Contains(t, stdout1, "step 3")
93
94 stdout2, _, done2, _ := bgShell.GetOutput()
95 require.True(t, done2)
96 require.Equal(t, stdout1, stdout2, "Multiple GetOutput calls should return same result")
97}
98
99func TestBackgroundShell_EmptyOutput(t *testing.T) {
100 t.Parallel()
101
102 if runtime.GOOS == "windows" {
103 t.Skip("This test is flacky on Windows for some reason")
104 }
105
106 workingDir := t.TempDir()
107 ctx := context.Background()
108
109 // Start a background shell with no output
110 bgManager := shell.GetBackgroundShellManager()
111 bgShell, err := bgManager.Start(ctx, workingDir, nil, "sleep 0.1", "")
112 require.NoError(t, err)
113 defer bgManager.Kill(bgShell.ID)
114
115 // Wait for completion
116 bgShell.Wait()
117
118 stdout, stderr, done, err := bgShell.GetOutput()
119 require.NoError(t, err)
120 require.Empty(t, stdout)
121 require.Empty(t, stderr)
122 require.True(t, done)
123}
124
125func TestBackgroundShell_ExitCode(t *testing.T) {
126 t.Parallel()
127
128 workingDir := t.TempDir()
129 ctx := context.Background()
130
131 // Start a background shell that exits with non-zero code
132 bgManager := shell.GetBackgroundShellManager()
133 bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'failing' && exit 42", "")
134 require.NoError(t, err)
135 defer bgManager.Kill(bgShell.ID)
136
137 // Wait for completion
138 bgShell.Wait()
139
140 stdout, _, done, execErr := bgShell.GetOutput()
141 require.True(t, done)
142 require.Contains(t, stdout, "failing")
143 require.Error(t, execErr)
144
145 exitCode := shell.ExitCode(execErr)
146 require.Equal(t, 42, exitCode)
147}
148
149func TestBackgroundShell_WithBlockFuncs(t *testing.T) {
150 t.Parallel()
151
152 workingDir := t.TempDir()
153 ctx := context.Background()
154
155 blockFuncs := []shell.BlockFunc{
156 shell.CommandsBlocker([]string{"curl", "wget"}),
157 }
158
159 // Start a background shell with a blocked command
160 bgManager := shell.GetBackgroundShellManager()
161 bgShell, err := bgManager.Start(ctx, workingDir, blockFuncs, "curl example.com", "")
162 require.NoError(t, err)
163 defer bgManager.Kill(bgShell.ID)
164
165 // Wait for completion
166 bgShell.Wait()
167
168 stdout, stderr, done, execErr := bgShell.GetOutput()
169 require.True(t, done)
170
171 // The command should have been blocked, check stderr or error
172 if execErr != nil {
173 // Error might contain the message
174 require.Contains(t, execErr.Error(), "not allowed")
175 } else {
176 // Or it might be in stderr
177 output := stdout + stderr
178 require.Contains(t, output, "not allowed")
179 }
180}
181
182func TestBackgroundShell_StdoutAndStderr(t *testing.T) {
183 t.Parallel()
184
185 workingDir := t.TempDir()
186 ctx := context.Background()
187
188 // Start a background shell with both stdout and stderr
189 bgManager := shell.GetBackgroundShellManager()
190 bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'stdout message' && echo 'stderr message' >&2", "")
191 require.NoError(t, err)
192 defer bgManager.Kill(bgShell.ID)
193
194 // Wait for completion
195 bgShell.Wait()
196
197 stdout, stderr, done, err := bgShell.GetOutput()
198 require.NoError(t, err)
199 require.True(t, done)
200 require.Contains(t, stdout, "stdout message")
201 require.Contains(t, stderr, "stderr message")
202}
203
204func TestBackgroundShell_ConcurrentAccess(t *testing.T) {
205 t.Parallel()
206
207 workingDir := t.TempDir()
208 ctx := context.Background()
209
210 // Start a background shell
211 bgManager := shell.GetBackgroundShellManager()
212 bgShell, err := bgManager.Start(ctx, workingDir, nil, "for i in 1 2 3 4 5; do echo \"line $i\"; sleep 0.05; done", "")
213 require.NoError(t, err)
214 defer bgManager.Kill(bgShell.ID)
215
216 // Access output concurrently from multiple goroutines
217 done := make(chan struct{})
218 errors := make(chan error, 10)
219
220 for range 10 {
221 go func() {
222 for {
223 select {
224 case <-done:
225 return
226 default:
227 _, _, _, err := bgShell.GetOutput()
228 if err != nil {
229 errors <- err
230 }
231 dir := bgShell.WorkingDir
232 if dir == "" {
233 errors <- err
234 }
235 time.Sleep(10 * time.Millisecond)
236 }
237 }
238 }()
239 }
240
241 // Let it run for a bit
242 time.Sleep(300 * time.Millisecond)
243 close(done)
244
245 // Check for any errors
246 select {
247 case err := <-errors:
248 t.Fatalf("Concurrent access caused error: %v", err)
249 case <-time.After(100 * time.Millisecond):
250 // No errors - success
251 }
252}
253
254func TestBackgroundShell_List(t *testing.T) {
255 t.Parallel()
256
257 workingDir := t.TempDir()
258 ctx := context.Background()
259
260 bgManager := shell.GetBackgroundShellManager()
261
262 // Start multiple background shells
263 shells := make([]*shell.BackgroundShell, 3)
264 for i := range 3 {
265 bgShell, err := bgManager.Start(ctx, workingDir, nil, "sleep 1", "")
266 require.NoError(t, err)
267 shells[i] = bgShell
268 }
269
270 // Get the list
271 ids := bgManager.List()
272
273 // Verify all our shells are in the list
274 for _, sh := range shells {
275 require.Contains(t, ids, sh.ID, "Shell %s not found in list", sh.ID)
276 }
277
278 // Clean up
279 for _, sh := range shells {
280 bgManager.Kill(sh.ID)
281 }
282}
283
284func TestBackgroundShell_AutoBackground(t *testing.T) {
285 t.Parallel()
286
287 workingDir := t.TempDir()
288 ctx := context.Background()
289
290 // Test that a quick command completes synchronously
291 t.Run("quick command completes synchronously", func(t *testing.T) {
292 t.Parallel()
293 bgManager := shell.GetBackgroundShellManager()
294 bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'quick'", "")
295 require.NoError(t, err)
296
297 // Wait threshold time
298 time.Sleep(5 * time.Second)
299
300 // Should be done by now
301 stdout, stderr, done, err := bgShell.GetOutput()
302 require.NoError(t, err)
303 require.True(t, done, "Quick command should be done")
304 require.Contains(t, stdout, "quick")
305 require.Empty(t, stderr)
306
307 // Clean up
308 bgManager.Kill(bgShell.ID)
309 })
310
311 // Test that a long command stays in background
312 t.Run("long command stays in background", func(t *testing.T) {
313 t.Parallel()
314 bgManager := shell.GetBackgroundShellManager()
315 bgShell, err := bgManager.Start(ctx, workingDir, nil, "sleep 20 && echo '20 seconds completed'", "")
316 require.NoError(t, err)
317 defer bgManager.Kill(bgShell.ID)
318
319 // Wait threshold time
320 time.Sleep(5 * time.Second)
321
322 // Should still be running
323 stdout, stderr, done, err := bgShell.GetOutput()
324 require.NoError(t, err)
325 require.False(t, done, "Long command should still be running")
326 require.Empty(t, stdout, "No output yet from sleep command")
327 require.Empty(t, stderr)
328
329 // Verify we can get the shell from manager
330 retrieved, ok := bgManager.Get(bgShell.ID)
331 require.True(t, ok, "Should be able to retrieve background shell")
332 require.Equal(t, bgShell.ID, retrieved.ID)
333 })
334}