@@ -36,6 +36,7 @@ import (
36
36
"github.com/coder/coder/v2/pty"
37
37
"github.com/coder/coder/v2/testutil"
38
38
"github.com/coder/quartz"
39
+ "github.com/coder/websocket"
39
40
)
40
41
41
42
// fakeContainerCLI implements the agentcontainers.ContainerCLI interface for
@@ -441,6 +442,178 @@ func TestAPI(t *testing.T) {
441
442
logbuf .Reset ()
442
443
})
443
444
445
+ t .Run ("Watch" ,func (t * testing.T ) {
446
+ t .Parallel ()
447
+
448
+ fakeContainer1 := fakeContainer (t ,func (c * codersdk.WorkspaceAgentContainer ) {
449
+ c .ID = "container1"
450
+ c .FriendlyName = "devcontainer1"
451
+ c .Image = "busybox:latest"
452
+ c .Labels = map [string ]string {
453
+ agentcontainers .DevcontainerLocalFolderLabel :"/home/coder/project1" ,
454
+ agentcontainers .DevcontainerConfigFileLabel :"/home/coder/project1/.devcontainer/devcontainer.json" ,
455
+ }
456
+ })
457
+
458
+ fakeContainer2 := fakeContainer (t ,func (c * codersdk.WorkspaceAgentContainer ) {
459
+ c .ID = "container2"
460
+ c .FriendlyName = "devcontainer2"
461
+ c .Image = "ubuntu:latest"
462
+ c .Labels = map [string ]string {
463
+ agentcontainers .DevcontainerLocalFolderLabel :"/home/coder/project2" ,
464
+ agentcontainers .DevcontainerConfigFileLabel :"/home/coder/project2/.devcontainer/devcontainer.json" ,
465
+ }
466
+ })
467
+
468
+ stages := []struct {
469
+ containers []codersdk.WorkspaceAgentContainer
470
+ expected codersdk.WorkspaceAgentListContainersResponse
471
+ }{
472
+ {
473
+ containers : []codersdk.WorkspaceAgentContainer {fakeContainer1 },
474
+ expected : codersdk.WorkspaceAgentListContainersResponse {
475
+ Containers : []codersdk.WorkspaceAgentContainer {fakeContainer1 },
476
+ Devcontainers : []codersdk.WorkspaceAgentDevcontainer {
477
+ {
478
+ Name :"project1" ,
479
+ WorkspaceFolder :fakeContainer1 .Labels [agentcontainers .DevcontainerLocalFolderLabel ],
480
+ ConfigPath :fakeContainer1 .Labels [agentcontainers .DevcontainerConfigFileLabel ],
481
+ Status :"running" ,
482
+ Container :& fakeContainer1 ,
483
+ },
484
+ },
485
+ },
486
+ },
487
+ {
488
+ containers : []codersdk.WorkspaceAgentContainer {fakeContainer1 ,fakeContainer2 },
489
+ expected : codersdk.WorkspaceAgentListContainersResponse {
490
+ Containers : []codersdk.WorkspaceAgentContainer {fakeContainer1 ,fakeContainer2 },
491
+ Devcontainers : []codersdk.WorkspaceAgentDevcontainer {
492
+ {
493
+ Name :"project1" ,
494
+ WorkspaceFolder :fakeContainer1 .Labels [agentcontainers .DevcontainerLocalFolderLabel ],
495
+ ConfigPath :fakeContainer1 .Labels [agentcontainers .DevcontainerConfigFileLabel ],
496
+ Status :"running" ,
497
+ Container :& fakeContainer1 ,
498
+ },
499
+ {
500
+ Name :"project2" ,
501
+ WorkspaceFolder :fakeContainer2 .Labels [agentcontainers .DevcontainerLocalFolderLabel ],
502
+ ConfigPath :fakeContainer2 .Labels [agentcontainers .DevcontainerConfigFileLabel ],
503
+ Status :"running" ,
504
+ Container :& fakeContainer2 ,
505
+ },
506
+ },
507
+ },
508
+ },
509
+ {
510
+ containers : []codersdk.WorkspaceAgentContainer {fakeContainer2 },
511
+ expected : codersdk.WorkspaceAgentListContainersResponse {
512
+ Containers : []codersdk.WorkspaceAgentContainer {fakeContainer2 },
513
+ Devcontainers : []codersdk.WorkspaceAgentDevcontainer {
514
+ {
515
+ Name :"" ,
516
+ WorkspaceFolder :fakeContainer1 .Labels [agentcontainers .DevcontainerLocalFolderLabel ],
517
+ ConfigPath :fakeContainer1 .Labels [agentcontainers .DevcontainerConfigFileLabel ],
518
+ Status :"stopped" ,
519
+ Container :nil ,
520
+ },
521
+ {
522
+ Name :"project2" ,
523
+ WorkspaceFolder :fakeContainer2 .Labels [agentcontainers .DevcontainerLocalFolderLabel ],
524
+ ConfigPath :fakeContainer2 .Labels [agentcontainers .DevcontainerConfigFileLabel ],
525
+ Status :"running" ,
526
+ Container :& fakeContainer2 ,
527
+ },
528
+ },
529
+ },
530
+ },
531
+ }
532
+
533
+ var (
534
+ ctx = testutil .Context (t ,testutil .WaitShort )
535
+ mClock = quartz .NewMock (t )
536
+ updaterTickerTrap = mClock .Trap ().TickerFunc ("updaterLoop" )
537
+ mCtrl = gomock .NewController (t )
538
+ mLister = acmock .NewMockContainerCLI (mCtrl )
539
+ logger = slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true }).Leveled (slog .LevelDebug )
540
+ )
541
+
542
+ // Set up initial state for immediate send on connection
543
+ mLister .EXPECT ().List (gomock .Any ()).Return (codersdk.WorkspaceAgentListContainersResponse {Containers :stages [0 ].containers },nil )
544
+ mLister .EXPECT ().DetectArchitecture (gomock .Any (),gomock .Any ()).Return ("<none>" ,nil ).AnyTimes ()
545
+
546
+ api := agentcontainers .NewAPI (logger ,
547
+ agentcontainers .WithClock (mClock ),
548
+ agentcontainers .WithContainerCLI (mLister ),
549
+ agentcontainers .WithWatcher (watcher .NewNoop ()),
550
+ )
551
+ api .Start ()
552
+ defer api .Close ()
553
+
554
+ srv := httptest .NewServer (api .Routes ())
555
+ defer srv .Close ()
556
+
557
+ updaterTickerTrap .MustWait (ctx ).MustRelease (ctx )
558
+ defer updaterTickerTrap .Close ()
559
+
560
+ client ,res ,err := websocket .Dial (ctx ,srv .URL + "/watch" ,nil )
561
+ require .NoError (t ,err )
562
+ if res != nil && res .Body != nil {
563
+ defer res .Body .Close ()
564
+ }
565
+
566
+ // Read initial state sent immediately on connection
567
+ mt ,msg ,err := client .Read (ctx )
568
+ require .NoError (t ,err )
569
+ require .Equal (t ,websocket .MessageText ,mt )
570
+
571
+ var got codersdk.WorkspaceAgentListContainersResponse
572
+ err = json .Unmarshal (msg ,& got )
573
+ require .NoError (t ,err )
574
+
575
+ require .Equal (t ,stages [0 ].expected .Containers ,got .Containers )
576
+ require .Len (t ,got .Devcontainers ,len (stages [0 ].expected .Devcontainers ))
577
+ for j ,expectedDev := range stages [0 ].expected .Devcontainers {
578
+ gotDev := got .Devcontainers [j ]
579
+ require .Equal (t ,expectedDev .Name ,gotDev .Name )
580
+ require .Equal (t ,expectedDev .WorkspaceFolder ,gotDev .WorkspaceFolder )
581
+ require .Equal (t ,expectedDev .ConfigPath ,gotDev .ConfigPath )
582
+ require .Equal (t ,expectedDev .Status ,gotDev .Status )
583
+ require .Equal (t ,expectedDev .Container ,gotDev .Container )
584
+ }
585
+
586
+ // Process remaining stages through updater loop
587
+ for i ,stage := range stages [1 :] {
588
+ mLister .EXPECT ().List (gomock .Any ()).Return (codersdk.WorkspaceAgentListContainersResponse {Containers :stage .containers },nil )
589
+
590
+ // Given: We allow the update loop to progress
591
+ _ ,aw := mClock .AdvanceNext ()
592
+ aw .MustWait (ctx )
593
+
594
+ // When: We attempt to read a message from the socket.
595
+ mt ,msg ,err := client .Read (ctx )
596
+ require .NoError (t ,err )
597
+ require .Equal (t ,websocket .MessageText ,mt )
598
+
599
+ // Then: We expect the receieved message matches the expected response.
600
+ var got codersdk.WorkspaceAgentListContainersResponse
601
+ err = json .Unmarshal (msg ,& got )
602
+ require .NoError (t ,err )
603
+
604
+ require .Equal (t ,stages [i + 1 ].expected .Containers ,got .Containers )
605
+ require .Len (t ,got .Devcontainers ,len (stages [i + 1 ].expected .Devcontainers ))
606
+ for j ,expectedDev := range stages [i + 1 ].expected .Devcontainers {
607
+ gotDev := got .Devcontainers [j ]
608
+ require .Equal (t ,expectedDev .Name ,gotDev .Name )
609
+ require .Equal (t ,expectedDev .WorkspaceFolder ,gotDev .WorkspaceFolder )
610
+ require .Equal (t ,expectedDev .ConfigPath ,gotDev .ConfigPath )
611
+ require .Equal (t ,expectedDev .Status ,gotDev .Status )
612
+ require .Equal (t ,expectedDev .Container ,gotDev .Container )
613
+ }
614
+ }
615
+ })
616
+
444
617
// List tests the API.getContainers method using a mock
445
618
// implementation. It specifically tests caching behavior.
446
619
t .Run ("List" ,func (t * testing.T ) {