bash_background_test.go

  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}