job_test.go

  1package tools
  2
  3import (
  4	"context"
  5	"testing"
  6	"time"
  7
  8	"git.secluded.site/crush/internal/shell"
  9	"github.com/stretchr/testify/require"
 10)
 11
 12func TestBackgroundShell_Integration(t *testing.T) {
 13	t.Parallel()
 14
 15	workingDir := t.TempDir()
 16	ctx := context.Background()
 17
 18	// Start a background shell
 19	bgManager := shell.GetBackgroundShellManager()
 20	bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'hello background' && echo 'done'", "")
 21	require.NoError(t, err)
 22	require.NotEmpty(t, bgShell.ID)
 23
 24	// Wait for completion
 25	bgShell.Wait()
 26
 27	// Check final output
 28	stdout, stderr, done, err := bgShell.GetOutput()
 29	require.NoError(t, err)
 30	require.Contains(t, stdout, "hello background")
 31	require.Contains(t, stdout, "done")
 32	require.True(t, done)
 33	require.Empty(t, stderr)
 34
 35	// Clean up
 36	bgManager.Kill(bgShell.ID)
 37}
 38
 39func TestBackgroundShell_Kill(t *testing.T) {
 40	t.Parallel()
 41
 42	workingDir := t.TempDir()
 43	ctx := context.Background()
 44
 45	// Start a long-running background shell
 46	bgManager := shell.GetBackgroundShellManager()
 47	bgShell, err := bgManager.Start(ctx, workingDir, nil, "sleep 100", "")
 48	require.NoError(t, err)
 49
 50	// Kill it
 51	err = bgManager.Kill(bgShell.ID)
 52	require.NoError(t, err)
 53
 54	// Verify it's gone
 55	_, ok := bgManager.Get(bgShell.ID)
 56	require.False(t, ok)
 57
 58	// Verify the shell is done
 59	require.True(t, bgShell.IsDone())
 60}
 61
 62func TestBackgroundShell_MultipleOutputCalls(t *testing.T) {
 63	t.Parallel()
 64
 65	workingDir := t.TempDir()
 66	ctx := context.Background()
 67
 68	// Start a background shell
 69	bgManager := shell.GetBackgroundShellManager()
 70	bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'step 1' && echo 'step 2' && echo 'step 3'", "")
 71	require.NoError(t, err)
 72	defer bgManager.Kill(bgShell.ID)
 73
 74	// Check that we can call GetOutput multiple times while running
 75	for range 5 {
 76		_, _, done, _ := bgShell.GetOutput()
 77		if done {
 78			break
 79		}
 80		time.Sleep(10 * time.Millisecond)
 81	}
 82
 83	// Wait for completion
 84	bgShell.Wait()
 85
 86	// Multiple calls after completion should return the same result
 87	stdout1, _, done1, _ := bgShell.GetOutput()
 88	require.True(t, done1)
 89	require.Contains(t, stdout1, "step 1")
 90	require.Contains(t, stdout1, "step 2")
 91	require.Contains(t, stdout1, "step 3")
 92
 93	stdout2, _, done2, _ := bgShell.GetOutput()
 94	require.True(t, done2)
 95	require.Equal(t, stdout1, stdout2, "Multiple GetOutput calls should return same result")
 96}
 97
 98func TestBackgroundShell_EmptyOutput(t *testing.T) {
 99	t.Parallel()
100
101	workingDir := t.TempDir()
102	ctx := context.Background()
103
104	// Start a background shell with no output
105	bgManager := shell.GetBackgroundShellManager()
106	bgShell, err := bgManager.Start(ctx, workingDir, nil, "sleep 0.1", "")
107	require.NoError(t, err)
108	defer bgManager.Kill(bgShell.ID)
109
110	// Wait for completion
111	bgShell.Wait()
112
113	stdout, stderr, done, err := bgShell.GetOutput()
114	require.NoError(t, err)
115	require.Empty(t, stdout)
116	require.Empty(t, stderr)
117	require.True(t, done)
118}
119
120func TestBackgroundShell_ExitCode(t *testing.T) {
121	t.Parallel()
122
123	workingDir := t.TempDir()
124	ctx := context.Background()
125
126	// Start a background shell that exits with non-zero code
127	bgManager := shell.GetBackgroundShellManager()
128	bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'failing' && exit 42", "")
129	require.NoError(t, err)
130	defer bgManager.Kill(bgShell.ID)
131
132	// Wait for completion
133	bgShell.Wait()
134
135	stdout, _, done, execErr := bgShell.GetOutput()
136	require.True(t, done)
137	require.Contains(t, stdout, "failing")
138	require.Error(t, execErr)
139
140	exitCode := shell.ExitCode(execErr)
141	require.Equal(t, 42, exitCode)
142}
143
144func TestBackgroundShell_WithBlockFuncs(t *testing.T) {
145	t.Parallel()
146
147	workingDir := t.TempDir()
148	ctx := context.Background()
149
150	blockFuncs := []shell.BlockFunc{
151		shell.CommandsBlocker([]string{"curl", "wget"}),
152	}
153
154	// Start a background shell with a blocked command
155	bgManager := shell.GetBackgroundShellManager()
156	bgShell, err := bgManager.Start(ctx, workingDir, blockFuncs, "curl example.com", "")
157	require.NoError(t, err)
158	defer bgManager.Kill(bgShell.ID)
159
160	// Wait for completion
161	bgShell.Wait()
162
163	stdout, stderr, done, execErr := bgShell.GetOutput()
164	require.True(t, done)
165
166	// The command should have been blocked, check stderr or error
167	if execErr != nil {
168		// Error might contain the message
169		require.Contains(t, execErr.Error(), "not allowed")
170	} else {
171		// Or it might be in stderr
172		output := stdout + stderr
173		require.Contains(t, output, "not allowed")
174	}
175}
176
177func TestBackgroundShell_StdoutAndStderr(t *testing.T) {
178	t.Parallel()
179
180	workingDir := t.TempDir()
181	ctx := context.Background()
182
183	// Start a background shell with both stdout and stderr
184	bgManager := shell.GetBackgroundShellManager()
185	bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'stdout message' && echo 'stderr message' >&2", "")
186	require.NoError(t, err)
187	defer bgManager.Kill(bgShell.ID)
188
189	// Wait for completion
190	bgShell.Wait()
191
192	stdout, stderr, done, err := bgShell.GetOutput()
193	require.NoError(t, err)
194	require.True(t, done)
195	require.Contains(t, stdout, "stdout message")
196	require.Contains(t, stderr, "stderr message")
197}
198
199func TestBackgroundShell_ConcurrentAccess(t *testing.T) {
200	t.Parallel()
201
202	workingDir := t.TempDir()
203	ctx := context.Background()
204
205	// Start a background shell
206	bgManager := shell.GetBackgroundShellManager()
207	bgShell, err := bgManager.Start(ctx, workingDir, nil, "for i in 1 2 3 4 5; do echo \"line $i\"; sleep 0.05; done", "")
208	require.NoError(t, err)
209	defer bgManager.Kill(bgShell.ID)
210
211	// Access output concurrently from multiple goroutines
212	done := make(chan struct{})
213	errors := make(chan error, 10)
214
215	for range 10 {
216		go func() {
217			for {
218				select {
219				case <-done:
220					return
221				default:
222					_, _, _, err := bgShell.GetOutput()
223					if err != nil {
224						errors <- err
225					}
226					dir := bgShell.WorkingDir
227					if dir == "" {
228						errors <- err
229					}
230					time.Sleep(10 * time.Millisecond)
231				}
232			}
233		}()
234	}
235
236	// Let it run for a bit
237	time.Sleep(300 * time.Millisecond)
238	close(done)
239
240	// Check for any errors
241	select {
242	case err := <-errors:
243		t.Fatalf("Concurrent access caused error: %v", err)
244	case <-time.After(100 * time.Millisecond):
245		// No errors - success
246	}
247}
248
249func TestBackgroundShell_List(t *testing.T) {
250	t.Parallel()
251
252	workingDir := t.TempDir()
253	ctx := context.Background()
254
255	bgManager := shell.GetBackgroundShellManager()
256
257	// Start multiple background shells
258	shells := make([]*shell.BackgroundShell, 3)
259	for i := range 3 {
260		bgShell, err := bgManager.Start(ctx, workingDir, nil, "sleep 1", "")
261		require.NoError(t, err)
262		shells[i] = bgShell
263	}
264
265	// Get the list
266	ids := bgManager.List()
267
268	// Verify all our shells are in the list
269	for _, sh := range shells {
270		require.Contains(t, ids, sh.ID, "Shell %s not found in list", sh.ID)
271	}
272
273	// Clean up
274	for _, sh := range shells {
275		bgManager.Kill(sh.ID)
276	}
277}
278
279func TestBackgroundShell_AutoBackground(t *testing.T) {
280	t.Parallel()
281
282	workingDir := t.TempDir()
283	ctx := context.Background()
284
285	// Test that a quick command completes synchronously
286	t.Run("quick command completes synchronously", func(t *testing.T) {
287		t.Parallel()
288		bgManager := shell.GetBackgroundShellManager()
289		bgShell, err := bgManager.Start(ctx, workingDir, nil, "echo 'quick'", "")
290		require.NoError(t, err)
291
292		// Wait threshold time
293		time.Sleep(5 * time.Second)
294
295		// Should be done by now
296		stdout, stderr, done, err := bgShell.GetOutput()
297		require.NoError(t, err)
298		require.True(t, done, "Quick command should be done")
299		require.Contains(t, stdout, "quick")
300		require.Empty(t, stderr)
301
302		// Clean up
303		bgManager.Kill(bgShell.ID)
304	})
305
306	// Test that a long command stays in background
307	t.Run("long command stays in background", func(t *testing.T) {
308		t.Parallel()
309		bgManager := shell.GetBackgroundShellManager()
310		bgShell, err := bgManager.Start(ctx, workingDir, nil, "sleep 20 && echo '20 seconds completed'", "")
311		require.NoError(t, err)
312		defer bgManager.Kill(bgShell.ID)
313
314		// Wait threshold time
315		time.Sleep(5 * time.Second)
316
317		// Should still be running
318		stdout, stderr, done, err := bgShell.GetOutput()
319		require.NoError(t, err)
320		require.False(t, done, "Long command should still be running")
321		require.Empty(t, stdout, "No output yet from sleep command")
322		require.Empty(t, stderr)
323
324		// Verify we can get the shell from manager
325		retrieved, ok := bgManager.Get(bgShell.ID)
326		require.True(t, ok, "Should be able to retrieve background shell")
327		require.Equal(t, bgShell.ID, retrieved.ID)
328	})
329}