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}