Skip to content

Commit d915910

Browse files
authored
fix(agent/agentcontainers): broadcast devcontainer dirty status over websocket (#21100)
1 parent 532a1f3 commit d915910

File tree

2 files changed

+73
-0
lines changed

2 files changed

+73
-0
lines changed

agent/agentcontainers/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,6 +1457,8 @@ func (api *API) markDevcontainerDirty(configPath string, modifiedAt time.Time) {
14571457

14581458
api.knownDevcontainers[dc.WorkspaceFolder] = dc
14591459
}
1460+
1461+
api.broadcastUpdatesLocked()
14601462
}
14611463

14621464
// cleanupSubAgents removes subagents that are no longer managed by

agent/agentcontainers/api_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,6 +1641,77 @@ func TestAPI(t *testing.T) {
16411641
require.NotNil(t, response.Devcontainers[0].Container, "container should not be nil")
16421642
})
16431643

1644+
// Verify that modifying a config file broadcasts the dirty status
1645+
// over websocket immediately.
1646+
t.Run("FileWatcherDirtyBroadcast", func(t *testing.T) {
1647+
t.Parallel()
1648+
1649+
ctx := testutil.Context(t, testutil.WaitShort)
1650+
configPath := "/workspace/project/.devcontainer/devcontainer.json"
1651+
fWatcher := newFakeWatcher(t)
1652+
fLister := &fakeContainerCLI{
1653+
containers: codersdk.WorkspaceAgentListContainersResponse{
1654+
Containers: []codersdk.WorkspaceAgentContainer{
1655+
{
1656+
ID: "container-id",
1657+
FriendlyName: "container-name",
1658+
Running: true,
1659+
Labels: map[string]string{
1660+
agentcontainers.DevcontainerLocalFolderLabel: "/workspace/project",
1661+
agentcontainers.DevcontainerConfigFileLabel: configPath,
1662+
},
1663+
},
1664+
},
1665+
},
1666+
}
1667+
1668+
mClock := quartz.NewMock(t)
1669+
tickerTrap := mClock.Trap().TickerFunc("updaterLoop")
1670+
1671+
api := agentcontainers.NewAPI(
1672+
slogtest.Make(t, nil).Leveled(slog.LevelDebug),
1673+
agentcontainers.WithContainerCLI(fLister),
1674+
agentcontainers.WithWatcher(fWatcher),
1675+
agentcontainers.WithClock(mClock),
1676+
)
1677+
api.Start()
1678+
defer api.Close()
1679+
1680+
srv := httptest.NewServer(api.Routes())
1681+
defer srv.Close()
1682+
1683+
tickerTrap.MustWait(ctx).MustRelease(ctx)
1684+
tickerTrap.Close()
1685+
1686+
wsConn, resp, err := websocket.Dial(ctx, "ws"+strings.TrimPrefix(srv.URL, "http")+"/watch", nil)
1687+
require.NoError(t, err)
1688+
if resp != nil && resp.Body != nil {
1689+
defer resp.Body.Close()
1690+
}
1691+
defer wsConn.Close(websocket.StatusNormalClosure, "")
1692+
1693+
// Read and discard initial state.
1694+
_, _, err = wsConn.Read(ctx)
1695+
require.NoError(t, err)
1696+
1697+
fWatcher.waitNext(ctx)
1698+
fWatcher.sendEventWaitNextCalled(ctx, fsnotify.Event{
1699+
Name: configPath,
1700+
Op: fsnotify.Write,
1701+
})
1702+
1703+
// Verify dirty status is broadcast without advancing the clock.
1704+
_, msg, err := wsConn.Read(ctx)
1705+
require.NoError(t, err)
1706+
1707+
var response codersdk.WorkspaceAgentListContainersResponse
1708+
err = json.Unmarshal(msg, &response)
1709+
require.NoError(t, err)
1710+
require.Len(t, response.Devcontainers, 1)
1711+
assert.True(t, response.Devcontainers[0].Dirty,
1712+
"devcontainer should be marked as dirty after config file modification")
1713+
})
1714+
16441715
t.Run("SubAgentLifecycle", func(t *testing.T) {
16451716
t.Parallel()
16461717

0 commit comments

Comments
 (0)