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, "sleep 10")
72 require.NoError(t, err)
73 defer bgManager.Kill(bgShell.ID)
74
75 // This should complete quickly without hanging
76 done := make(chan struct{})
77 go func() {
78 _, _, _, err := bgShell.GetOutput()
79 require.NoError(t, err)
80 close(done)
81 }()
82
83 select {
84 case <-done:
85 // Success - didn't hang
86 case <-time.After(2 * time.Second):
87 t.Fatal("GetOutput() hung - did not complete within timeout")
88 }
89}
90
91func TestBackgroundShell_MultipleOutputCalls(t *testing.T) {
92 t.Parallel()
93
94 workingDir := t.TempDir()
95 ctx := context.Background()
96
97 // Start a background shell
98 bgManager := shell.GetBackgroundShellManager()
99 bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'step 1' && echo 'step 2' && echo 'step 3'")
100 require.NoError(t, err)
101 defer bgManager.Kill(bgShell.ID)
102
103 // Check that we can call GetOutput multiple times while running
104 for range 5 {
105 _, _, done, _ := bgShell.GetOutput()
106 if done {
107 break
108 }
109 time.Sleep(10 * time.Millisecond)
110 }
111
112 // Wait for completion
113 bgShell.Wait()
114
115 // Multiple calls after completion should return the same result
116 stdout1, _, done1, _ := bgShell.GetOutput()
117 require.True(t, done1)
118 require.Contains(t, stdout1, "step 1")
119 require.Contains(t, stdout1, "step 2")
120 require.Contains(t, stdout1, "step 3")
121
122 stdout2, _, done2, _ := bgShell.GetOutput()
123 require.True(t, done2)
124 require.Equal(t, stdout1, stdout2, "Multiple GetOutput calls should return same result")
125}
126
127func TestBackgroundShell_EmptyOutput(t *testing.T) {
128 t.Parallel()
129
130 workingDir := t.TempDir()
131 ctx := context.Background()
132
133 // Start a background shell with no output
134 bgManager := shell.GetBackgroundShellManager()
135 bgShell, err := bgManager.Start(ctx, workingDir, nil, "sleep 0.1")
136 require.NoError(t, err)
137 defer bgManager.Kill(bgShell.ID)
138
139 // Wait for completion
140 bgShell.Wait()
141
142 stdout, stderr, done, err := bgShell.GetOutput()
143 require.NoError(t, err)
144 require.Empty(t, stdout)
145 require.Empty(t, stderr)
146 require.True(t, done)
147}
148
149func TestBackgroundShell_ExitCode(t *testing.T) {
150 t.Parallel()
151
152 workingDir := t.TempDir()
153 ctx := context.Background()
154
155 // Start a background shell that exits with non-zero code
156 bgManager := shell.GetBackgroundShellManager()
157 bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'failing' && exit 42")
158 require.NoError(t, err)
159 defer bgManager.Kill(bgShell.ID)
160
161 // Wait for completion
162 bgShell.Wait()
163
164 stdout, _, done, execErr := bgShell.GetOutput()
165 require.True(t, done)
166 require.Contains(t, stdout, "failing")
167 require.Error(t, execErr)
168
169 exitCode := shell.ExitCode(execErr)
170 require.Equal(t, 42, exitCode)
171}
172
173func TestBackgroundShell_WithBlockFuncs(t *testing.T) {
174 t.Parallel()
175
176 workingDir := t.TempDir()
177 ctx := context.Background()
178
179 blockFuncs := []shell.BlockFunc{
180 shell.CommandsBlocker([]string{"curl", "wget"}),
181 }
182
183 // Start a background shell with a blocked command
184 bgManager := shell.GetBackgroundShellManager()
185 bgShell, err := bgManager.Start(ctx, workingDir, blockFuncs, "curl example.com")
186 require.NoError(t, err)
187 defer bgManager.Kill(bgShell.ID)
188
189 // Wait for completion
190 bgShell.Wait()
191
192 stdout, stderr, done, execErr := bgShell.GetOutput()
193 require.True(t, done)
194
195 // The command should have been blocked, check stderr or error
196 if execErr != nil {
197 // Error might contain the message
198 require.Contains(t, execErr.Error(), "not allowed")
199 } else {
200 // Or it might be in stderr
201 output := stdout + stderr
202 require.Contains(t, output, "not allowed")
203 }
204}
205
206func TestBackgroundShell_StdoutAndStderr(t *testing.T) {
207 t.Parallel()
208
209 workingDir := t.TempDir()
210 ctx := context.Background()
211
212 // Start a background shell with both stdout and stderr
213 bgManager := shell.GetBackgroundShellManager()
214 bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'stdout message' && echo 'stderr message' >&2")
215 require.NoError(t, err)
216 defer bgManager.Kill(bgShell.ID)
217
218 // Wait for completion
219 bgShell.Wait()
220
221 stdout, stderr, done, err := bgShell.GetOutput()
222 require.NoError(t, err)
223 require.True(t, done)
224 require.Contains(t, stdout, "stdout message")
225 require.Contains(t, stderr, "stderr message")
226}
227
228func TestBackgroundShell_ConcurrentAccess(t *testing.T) {
229 t.Parallel()
230
231 workingDir := t.TempDir()
232 ctx := context.Background()
233
234 // Start a background shell
235 bgManager := shell.GetBackgroundShellManager()
236 bgShell, err := bgManager.Start(ctx, workingDir, nil, "for i in 1 2 3 4 5; do echo \"line $i\"; sleep 0.05; done")
237 require.NoError(t, err)
238 defer bgManager.Kill(bgShell.ID)
239
240 // Access output concurrently from multiple goroutines
241 done := make(chan struct{})
242 errors := make(chan error, 10)
243
244 for range 10 {
245 go func() {
246 for {
247 select {
248 case <-done:
249 return
250 default:
251 _, _, _, err := bgShell.GetOutput()
252 if err != nil {
253 errors <- err
254 }
255 dir := bgShell.GetWorkingDir()
256 if dir == "" {
257 errors <- err
258 }
259 time.Sleep(10 * time.Millisecond)
260 }
261 }
262 }()
263 }
264
265 // Let it run for a bit
266 time.Sleep(300 * time.Millisecond)
267 close(done)
268
269 // Check for any errors
270 select {
271 case err := <-errors:
272 t.Fatalf("Concurrent access caused error: %v", err)
273 case <-time.After(100 * time.Millisecond):
274 // No errors - success
275 }
276}
277
278func TestBackgroundShell_List(t *testing.T) {
279 t.Parallel()
280
281 workingDir := t.TempDir()
282 ctx := context.Background()
283
284 bgManager := shell.GetBackgroundShellManager()
285
286 // Start multiple background shells
287 shells := make([]*shell.BackgroundShell, 3)
288 for i := range 3 {
289 bgShell, err := bgManager.Start(ctx, workingDir, nil, "sleep 1")
290 require.NoError(t, err)
291 shells[i] = bgShell
292 }
293
294 // Get the list
295 ids := bgManager.List()
296
297 // Verify all our shells are in the list
298 for _, sh := range shells {
299 require.Contains(t, ids, sh.ID, "Shell %s not found in list", sh.ID)
300 }
301
302 // Clean up
303 for _, sh := range shells {
304 bgManager.Kill(sh.ID)
305 }
306}
307
308func TestBackgroundShell_IDFormat(t *testing.T) {
309 t.Parallel()
310
311 workingDir := t.TempDir()
312 ctx := context.Background()
313
314 bgManager := shell.GetBackgroundShellManager()
315 bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'test'")
316 require.NoError(t, err)
317 defer bgManager.Kill(bgShell.ID)
318
319 // Verify ID is human-readable (hotdiva2000 format)
320 // Should contain hyphens and be readable
321 require.NotEmpty(t, bgShell.ID)
322 require.Contains(t, bgShell.ID, "-", "ID should be human-readable with hyphens")
323
324 // Should not be a UUID format
325 require.False(t, strings.Contains(bgShell.ID, "uuid"), "ID should not be UUID format")
326
327 // Length should be reasonable for human-readable IDs
328 require.Greater(t, len(bgShell.ID), 5, "ID should be long enough")
329 require.Less(t, len(bgShell.ID), 100, "ID should not be too long")
330}
331
332func TestBackgroundShell_AutoBackground(t *testing.T) {
333 t.Parallel()
334
335 workingDir := t.TempDir()
336 ctx := context.Background()
337
338 // Test that a quick command completes synchronously
339 t.Run("quick command completes synchronously", func(t *testing.T) {
340 t.Parallel()
341 bgManager := shell.GetBackgroundShellManager()
342 bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'quick'")
343 require.NoError(t, err)
344
345 // Wait threshold time
346 time.Sleep(5 * time.Second)
347
348 // Should be done by now
349 stdout, stderr, done, err := bgShell.GetOutput()
350 require.NoError(t, err)
351 require.True(t, done, "Quick command should be done")
352 require.Contains(t, stdout, "quick")
353 require.Empty(t, stderr)
354
355 // Clean up
356 bgManager.Kill(bgShell.ID)
357 })
358
359 // Test that a long command stays in background
360 t.Run("long command stays in background", func(t *testing.T) {
361 t.Parallel()
362 bgManager := shell.GetBackgroundShellManager()
363 bgShell, err := bgManager.Start(ctx, workingDir, nil, "sleep 20 && echo '20 seconds completed'")
364 require.NoError(t, err)
365 defer bgManager.Kill(bgShell.ID)
366
367 // Wait threshold time
368 time.Sleep(5 * time.Second)
369
370 // Should still be running
371 stdout, stderr, done, err := bgShell.GetOutput()
372 require.NoError(t, err)
373 require.False(t, done, "Long command should still be running")
374 require.Empty(t, stdout, "No output yet from sleep command")
375 require.Empty(t, stderr)
376
377 // Verify we can get the shell from manager
378 retrieved, ok := bgManager.Get(bgShell.ID)
379 require.True(t, ok, "Should be able to retrieve background shell")
380 require.Equal(t, bgShell.ID, retrieved.ID)
381 })
382
383 // Test that we can check output of long-running command later
384 t.Run("can check output after completion", func(t *testing.T) {
385 t.Parallel()
386 bgManager := shell.GetBackgroundShellManager()
387 bgShell, err := bgManager.Start(ctx, workingDir, nil, "sleep 3 && echo 'completed'")
388 require.NoError(t, err)
389 defer bgManager.Kill(bgShell.ID)
390
391 // Initially should be running
392 _, _, done, _ := bgShell.GetOutput()
393 require.False(t, done, "Should be running initially")
394
395 // Wait for completion
396 time.Sleep(4 * time.Second)
397
398 // Now should be done
399 stdout, stderr, done, err := bgShell.GetOutput()
400 require.NoError(t, err)
401 require.True(t, done, "Should be done after waiting")
402 require.Contains(t, stdout, "completed")
403 require.Empty(t, stderr)
404 })
405}