@@ -60,11 +60,14 @@ func (f *fakeContainerCLI) ExecAs(ctx context.Context, name, user string, args .
60
60
// fakeDevcontainerCLI implements the agentcontainers.DevcontainerCLI
61
61
// interface for testing.
62
62
type fakeDevcontainerCLI struct {
63
- upID string
64
- upErr error
65
- upErrC chan error // If set, send to return err, close to return upErr.
66
- execErr error
67
- execErrC chan func (cmd string , args ... string ) error // If set, send fn to return err, nil or close to return execErr.
63
+ upID string
64
+ upErr error
65
+ upErrC chan error // If set, send to return err, close to return upErr.
66
+ execErr error
67
+ execErrC chan func (cmd string , args ... string ) error // If set, send fn to return err, nil or close to return execErr.
68
+ readConfig agentcontainers.DevcontainerConfig
69
+ readConfigErr error
70
+ readConfigErrC chan error
68
71
}
69
72
70
73
func (f * fakeDevcontainerCLI ) Up (ctx context.Context , _ , _ string , _ ... agentcontainers.DevcontainerCLIUpOptions ) (string , error ) {
@@ -95,6 +98,20 @@ func (f *fakeDevcontainerCLI) Exec(ctx context.Context, _, _ string, cmd string,
95
98
return f .execErr
96
99
}
97
100
101
+ func (f * fakeDevcontainerCLI ) ReadConfig (ctx context.Context , _ , _ string , _ ... agentcontainers.DevcontainerCLIReadConfigOptions ) (agentcontainers.DevcontainerConfig , error ) {
102
+ if f .readConfigErrC != nil {
103
+ select {
104
+ case <- ctx .Done ():
105
+ return agentcontainers.DevcontainerConfig {}, ctx .Err ()
106
+ case err , ok := <- f .readConfigErrC :
107
+ if ok {
108
+ return f .readConfig , err
109
+ }
110
+ }
111
+ }
112
+ return f .readConfig , f .readConfigErr
113
+ }
114
+
98
115
// fakeWatcher implements the watcher.Watcher interface for testing.
99
116
// It allows controlling what events are sent and when.
100
117
type fakeWatcher struct {
@@ -1132,10 +1149,12 @@ func TestAPI(t *testing.T) {
1132
1149
Containers : []codersdk.WorkspaceAgentContainer {container },
1133
1150
},
1134
1151
}
1152
+ fDCCLI := & fakeDevcontainerCLI {}
1135
1153
1136
1154
logger := slogtest .Make (t , nil ).Leveled (slog .LevelDebug )
1137
1155
api := agentcontainers .NewAPI (
1138
1156
logger ,
1157
+ agentcontainers .WithDevcontainerCLI (fDCCLI ),
1139
1158
agentcontainers .WithContainerCLI (fLister ),
1140
1159
agentcontainers .WithWatcher (fWatcher ),
1141
1160
agentcontainers .WithClock (mClock ),
@@ -1425,6 +1444,130 @@ func TestAPI(t *testing.T) {
1425
1444
assert .Contains (t , fakeSAC .deleted , existingAgentID )
1426
1445
assert .Empty (t , fakeSAC .agents )
1427
1446
})
1447
+
1448
+ t .Run ("Create" , func (t * testing.T ) {
1449
+ t .Parallel ()
1450
+
1451
+ if runtime .GOOS == "windows" {
1452
+ t .Skip ("Dev Container tests are not supported on Windows (this test uses mocks but fails due to Windows paths)" )
1453
+ }
1454
+
1455
+ tests := []struct {
1456
+ name string
1457
+ customization * agentcontainers.CoderCustomization
1458
+ afterCreate func (t * testing.T , subAgent agentcontainers.SubAgent )
1459
+ }{
1460
+ {
1461
+ name : "WithoutCustomization" ,
1462
+ customization : nil ,
1463
+ },
1464
+ {
1465
+ name : "WithDisplayApps" ,
1466
+ customization : & agentcontainers.CoderCustomization {
1467
+ DisplayApps : []codersdk.DisplayApp {
1468
+ codersdk .DisplayAppSSH ,
1469
+ codersdk .DisplayAppWebTerminal ,
1470
+ codersdk .DisplayAppVSCodeInsiders ,
1471
+ },
1472
+ },
1473
+ afterCreate : func (t * testing.T , subAgent agentcontainers.SubAgent ) {
1474
+ require .Len (t , subAgent .DisplayApps , 3 )
1475
+ assert .Equal (t , codersdk .DisplayAppSSH , subAgent .DisplayApps [0 ])
1476
+ assert .Equal (t , codersdk .DisplayAppWebTerminal , subAgent .DisplayApps [1 ])
1477
+ assert .Equal (t , codersdk .DisplayAppVSCodeInsiders , subAgent .DisplayApps [2 ])
1478
+ },
1479
+ },
1480
+ }
1481
+
1482
+ for _ , tt := range tests {
1483
+ t .Run (tt .name , func (t * testing.T ) {
1484
+ t .Parallel ()
1485
+
1486
+ var (
1487
+ ctx = testutil .Context (t , testutil .WaitMedium )
1488
+ logger = testutil .Logger (t )
1489
+ mClock = quartz .NewMock (t )
1490
+ mCCLI = acmock .NewMockContainerCLI (gomock .NewController (t ))
1491
+ fSAC = & fakeSubAgentClient {createErrC : make (chan error , 1 )}
1492
+ fDCCLI = & fakeDevcontainerCLI {
1493
+ readConfig : agentcontainers.DevcontainerConfig {
1494
+ Configuration : agentcontainers.DevcontainerConfiguration {
1495
+ Customizations : agentcontainers.DevcontainerCustomizations {
1496
+ Coder : tt .customization ,
1497
+ },
1498
+ },
1499
+ },
1500
+ execErrC : make (chan func (cmd string , args ... string ) error , 1 ),
1501
+ }
1502
+
1503
+ testContainer = codersdk.WorkspaceAgentContainer {
1504
+ ID : "test-container-id" ,
1505
+ FriendlyName : "test-container" ,
1506
+ Image : "test-image" ,
1507
+ Running : true ,
1508
+ CreatedAt : time .Now (),
1509
+ Labels : map [string ]string {
1510
+ agentcontainers .DevcontainerLocalFolderLabel : "/workspaces" ,
1511
+ agentcontainers .DevcontainerConfigFileLabel : "/workspace/.devcontainer/devcontainer.json" ,
1512
+ },
1513
+ }
1514
+ )
1515
+
1516
+ coderBin , err := os .Executable ()
1517
+ require .NoError (t , err )
1518
+
1519
+ // Mock the `List` function to always return out test container.
1520
+ mCCLI .EXPECT ().List (gomock .Any ()).Return (codersdk.WorkspaceAgentListContainersResponse {
1521
+ Containers : []codersdk.WorkspaceAgentContainer {testContainer },
1522
+ }, nil ).AnyTimes ()
1523
+
1524
+ // Mock the steps used for injecting the coder agent.
1525
+ gomock .InOrder (
1526
+ mCCLI .EXPECT ().DetectArchitecture (gomock .Any (), testContainer .ID ).Return (runtime .GOARCH , nil ),
1527
+ mCCLI .EXPECT ().ExecAs (gomock .Any (), testContainer .ID , "root" , "mkdir" , "-p" , "/.coder-agent" ).Return (nil , nil ),
1528
+ mCCLI .EXPECT ().Copy (gomock .Any (), testContainer .ID , coderBin , "/.coder-agent/coder" ).Return (nil ),
1529
+ mCCLI .EXPECT ().ExecAs (gomock .Any (), testContainer .ID , "root" , "chmod" , "0755" , "/.coder-agent" , "/.coder-agent/coder" ).Return (nil , nil ),
1530
+ )
1531
+
1532
+ mClock .Set (time .Now ()).MustWait (ctx )
1533
+ tickerTrap := mClock .Trap ().TickerFunc ("updaterLoop" )
1534
+
1535
+ api := agentcontainers .NewAPI (logger ,
1536
+ agentcontainers .WithClock (mClock ),
1537
+ agentcontainers .WithContainerCLI (mCCLI ),
1538
+ agentcontainers .WithDevcontainerCLI (fDCCLI ),
1539
+ agentcontainers .WithSubAgentClient (fSAC ),
1540
+ agentcontainers .WithSubAgentURL ("test-subagent-url" ),
1541
+ agentcontainers .WithWatcher (watcher .NewNoop ()),
1542
+ )
1543
+ defer api .Close ()
1544
+
1545
+ // Close before api.Close() defer to avoid deadlock after test.
1546
+ defer close (fSAC .createErrC )
1547
+ defer close (fDCCLI .execErrC )
1548
+
1549
+ // Given: We allow agent creation and injection to succeed.
1550
+ testutil .RequireSend (ctx , t , fSAC .createErrC , nil )
1551
+ testutil .RequireSend (ctx , t , fDCCLI .execErrC , func (cmd string , args ... string ) error {
1552
+ assert .Equal (t , "pwd" , cmd )
1553
+ assert .Empty (t , args )
1554
+ return nil
1555
+ })
1556
+
1557
+ // Wait until the ticker has been registered.
1558
+ tickerTrap .MustWait (ctx ).MustRelease (ctx )
1559
+ tickerTrap .Close ()
1560
+
1561
+ // Then: We expected it to succeed
1562
+ require .Len (t , fSAC .created , 1 )
1563
+ assert .Equal (t , testContainer .FriendlyName , fSAC .created [0 ].Name )
1564
+
1565
+ if tt .afterCreate != nil {
1566
+ tt .afterCreate (t , fSAC .created [0 ])
1567
+ }
1568
+ })
1569
+ }
1570
+ })
1428
1571
}
1429
1572
1430
1573
// mustFindDevcontainerByPath returns the devcontainer with the given workspace
0 commit comments