9
9
"github.com/coder/coder/v2/agent/agenttest"
10
10
"github.com/coder/coder/v2/coderd/coderdtest"
11
11
"github.com/coder/coder/v2/codersdk/toolsdk"
12
+ "github.com/coder/coder/v2/testutil"
12
13
)
13
14
14
15
func TestWorkspaceBash (t * testing.T ) {
@@ -174,8 +175,6 @@ func TestWorkspaceBashTimeout(t *testing.T) {
174
175
175
176
// Test that the TimeoutMs field can be set and read correctly
176
177
args := toolsdk.WorkspaceBashArgs {
177
- Workspace :"test-workspace" ,
178
- Command :"echo test" ,
179
178
TimeoutMs :0 ,// Should default to 60000 in handler
180
179
}
181
180
@@ -192,8 +191,6 @@ func TestWorkspaceBashTimeout(t *testing.T) {
192
191
193
192
// Test that negative values can be set and will be handled by the default logic
194
193
args := toolsdk.WorkspaceBashArgs {
195
- Workspace :"test-workspace" ,
196
- Command :"echo test" ,
197
194
TimeoutMs :- 100 ,
198
195
}
199
196
@@ -279,7 +276,7 @@ func TestWorkspaceBashTimeoutIntegration(t *testing.T) {
279
276
TimeoutMs :2000 ,// 2 seconds timeout - should timeout after first echo
280
277
}
281
278
282
- result ,err := toolsdk . WorkspaceBash . Handler ( t . Context () ,deps ,args )
279
+ result ,err := testTool ( t , toolsdk . WorkspaceBash ,deps ,args )
283
280
284
281
// Should not error (timeout is handled gracefully)
285
282
require .NoError (t ,err )
@@ -313,15 +310,15 @@ func TestWorkspaceBashTimeoutIntegration(t *testing.T) {
313
310
314
311
deps ,err := toolsdk .NewDeps (client )
315
312
require .NoError (t ,err )
316
- ctx := context .Background ()
317
313
318
314
args := toolsdk.WorkspaceBashArgs {
319
315
Workspace :workspace .Name ,
320
316
Command :`echo "normal command"` ,// Quick command that should complete normally
321
317
TimeoutMs :5000 ,// 5 second timeout - plenty of time
322
318
}
323
319
324
- result ,err := toolsdk .WorkspaceBash .Handler (ctx ,deps ,args )
320
+ // Use testTool to register the tool as tested and satisfy coverage validation
321
+ result ,err := testTool (t ,toolsdk .WorkspaceBash ,deps ,args )
325
322
326
323
// Should not error
327
324
require .NoError (t ,err )
@@ -338,3 +335,142 @@ func TestWorkspaceBashTimeoutIntegration(t *testing.T) {
338
335
require .NotContains (t ,result .Output ,"Command canceled due to timeout" )
339
336
})
340
337
}
338
+
339
+ func TestWorkspaceBashBackgroundIntegration (t * testing.T ) {
340
+ t .Parallel ()
341
+
342
+ t .Run ("BackgroundCommandCapturesOutput" ,func (t * testing.T ) {
343
+ t .Parallel ()
344
+
345
+ client ,workspace ,agentToken := setupWorkspaceForAgent (t )
346
+
347
+ // Start the agent and wait for it to be fully ready
348
+ _ = agenttest .New (t ,client .URL ,agentToken )
349
+
350
+ // Wait for workspace agents to be ready
351
+ coderdtest .NewWorkspaceAgentWaiter (t ,client ,workspace .ID ).Wait ()
352
+
353
+ deps ,err := toolsdk .NewDeps (client )
354
+ require .NoError (t ,err )
355
+
356
+ args := toolsdk.WorkspaceBashArgs {
357
+ Workspace :workspace .Name ,
358
+ Command :`echo "started" && sleep 60 && echo "completed"` ,// Command that would take 60+ seconds
359
+ Background :true ,// Run in background
360
+ TimeoutMs :2000 ,// 2 second timeout
361
+ }
362
+
363
+ result ,err := testTool (t ,toolsdk .WorkspaceBash ,deps ,args )
364
+
365
+ // Should not error
366
+ require .NoError (t ,err )
367
+
368
+ t .Logf ("Background result: exitCode=%d, output=%q" ,result .ExitCode ,result .Output )
369
+
370
+ // Should have exit code 124 (timeout) since command times out
371
+ require .Equal (t ,124 ,result .ExitCode )
372
+
373
+ // Should capture output up to timeout point
374
+ require .Contains (t ,result .Output ,"started" ,"Should contain output captured before timeout" )
375
+
376
+ // Should NOT contain the second echo (it never executed due to timeout)
377
+ require .NotContains (t ,result .Output ,"completed" ,"Should not contain output after timeout" )
378
+
379
+ // Should contain background continuation message
380
+ require .Contains (t ,result .Output ,"Command continues running in background" )
381
+ })
382
+
383
+ t .Run ("BackgroundVsNormalExecution" ,func (t * testing.T ) {
384
+ t .Parallel ()
385
+
386
+ client ,workspace ,agentToken := setupWorkspaceForAgent (t )
387
+
388
+ // Start the agent and wait for it to be fully ready
389
+ _ = agenttest .New (t ,client .URL ,agentToken )
390
+
391
+ // Wait for workspace agents to be ready
392
+ coderdtest .NewWorkspaceAgentWaiter (t ,client ,workspace .ID ).Wait ()
393
+
394
+ deps ,err := toolsdk .NewDeps (client )
395
+ require .NoError (t ,err )
396
+
397
+ // First run the same command in normal mode
398
+ normalArgs := toolsdk.WorkspaceBashArgs {
399
+ Workspace :workspace .Name ,
400
+ Command :`echo "hello world"` ,
401
+ Background :false ,
402
+ }
403
+
404
+ normalResult ,err := toolsdk .WorkspaceBash .Handler (t .Context (),deps ,normalArgs )
405
+ require .NoError (t ,err )
406
+
407
+ // Normal mode should return the actual output
408
+ require .Equal (t ,0 ,normalResult .ExitCode )
409
+ require .Equal (t ,"hello world" ,normalResult .Output )
410
+
411
+ // Now run the same command in background mode
412
+ backgroundArgs := toolsdk.WorkspaceBashArgs {
413
+ Workspace :workspace .Name ,
414
+ Command :`echo "hello world"` ,
415
+ Background :true ,
416
+ }
417
+
418
+ backgroundResult ,err := testTool (t ,toolsdk .WorkspaceBash ,deps ,backgroundArgs )
419
+ require .NoError (t ,err )
420
+
421
+ t .Logf ("Normal result: %q" ,normalResult .Output )
422
+ t .Logf ("Background result: %q" ,backgroundResult .Output )
423
+
424
+ // Background mode should also return the actual output since command completes quickly
425
+ require .Equal (t ,0 ,backgroundResult .ExitCode )
426
+ require .Equal (t ,"hello world" ,backgroundResult .Output )
427
+ })
428
+
429
+ t .Run ("BackgroundCommandContinuesAfterTimeout" ,func (t * testing.T ) {
430
+ t .Parallel ()
431
+
432
+ client ,workspace ,agentToken := setupWorkspaceForAgent (t )
433
+
434
+ // Start the agent and wait for it to be fully ready
435
+ _ = agenttest .New (t ,client .URL ,agentToken )
436
+
437
+ // Wait for workspace agents to be ready
438
+ coderdtest .NewWorkspaceAgentWaiter (t ,client ,workspace .ID ).Wait ()
439
+
440
+ deps ,err := toolsdk .NewDeps (client )
441
+ require .NoError (t ,err )
442
+
443
+ args := toolsdk.WorkspaceBashArgs {
444
+ Workspace :workspace .Name ,
445
+ Command :`echo "started" && sleep 4 && echo "done" > /tmp/bg-test-done` ,// Command that will timeout but continue
446
+ TimeoutMs :2000 ,// 2000ms timeout (shorter than command duration)
447
+ Background :true ,// Run in background
448
+ }
449
+
450
+ result ,err := testTool (t ,toolsdk .WorkspaceBash ,deps ,args )
451
+
452
+ // Should not error but should timeout
453
+ require .NoError (t ,err )
454
+
455
+ t .Logf ("Background with timeout result: exitCode=%d, output=%q" ,result .ExitCode ,result .Output )
456
+
457
+ // Should have timeout exit code
458
+ require .Equal (t ,124 ,result .ExitCode )
459
+
460
+ // Should capture output before timeout
461
+ require .Contains (t ,result .Output ,"started" ,"Should contain output captured before timeout" )
462
+
463
+ // Should contain background continuation message
464
+ require .Contains (t ,result .Output ,"Command continues running in background" )
465
+
466
+ // Wait for the background command to complete (even though SSH session timed out)
467
+ require .Eventually (t ,func ()bool {
468
+ checkArgs := toolsdk.WorkspaceBashArgs {
469
+ Workspace :workspace .Name ,
470
+ Command :`cat /tmp/bg-test-done 2>/dev/null || echo "not found"` ,
471
+ }
472
+ checkResult ,err := toolsdk .WorkspaceBash .Handler (t .Context (),deps ,checkArgs )
473
+ return err == nil && checkResult .Output == "done"
474
+ },testutil .WaitMedium ,testutil .IntervalMedium ,"Background command should continue running and complete after timeout" )
475
+ })
476
+ }