job_test.go

  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}