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