job_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_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}