From 2d79ecc38154f1886790b1ec49576e2360c31884 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 17 Jun 2025 10:56:02 +0000 Subject: [PATCH 1/5] fix(agent/agentcontainers): reduce need to recreate sub agents Updates #18332 --- agent/agent_test.go | 23 ++++++-- agent/agentcontainers/api.go | 94 ++++++++++++++++++------------- agent/agentcontainers/api_test.go | 89 ++++++++++++++++------------- agent/agentcontainers/subagent.go | 21 +++++++ 4 files changed, 145 insertions(+), 82 deletions(-) diff --git a/agent/agent_test.go b/agent/agent_test.go index 9a8073a289b5f..55b1808784aa6 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -2080,6 +2080,10 @@ func TestAgent_DevcontainerAutostart(t *testing.T) { subAgentConnected := make(chan subAgentRequestPayload, 1) subAgentReady := make(chan struct{}, 1) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v2/workspaceagents/me/") { + return + } + t.Logf("Sub-agent request received: %s %s", r.Method, r.URL.Path) if r.Method != http.MethodPost { @@ -2226,11 +2230,22 @@ func TestAgent_DevcontainerAutostart(t *testing.T) { // Ensure the container update routine runs. tickerFuncTrap.MustWait(ctx).MustRelease(ctx) tickerFuncTrap.Close() - _, next := mClock.AdvanceNext() - next.MustWait(ctx) - // Verify that a subagent was created. - subAgents := agentClient.GetSubAgents() + // Since the agent does RefreshContainers, and the ticker function + // is set to skip instead of queue, we must advance the clock + // multiple times to ensure that the sub-agent is created. + var subAgents []*proto.SubAgent + for { + _, next := mClock.AdvanceNext() + next.MustWait(ctx) + + // Verify that a subagent was created. + subAgents = agentClient.GetSubAgents() + if len(subAgents) > 0 { + t.Logf("Found sub-agents: %d", len(subAgents)) + break + } + } require.Len(t, subAgents, 1, "expected one sub agent") subAgent := subAgents[0] diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index cdc4992022a85..40a4999f0c2c7 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -977,7 +977,7 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c ) // Check if subagent already exists for this devcontainer. - recreateSubAgent := false + maybeRecreateSubAgent := false proc, injected := api.injectedSubAgentProcs[dc.WorkspaceFolder] if injected { if proc.containerID == container.ID && proc.ctx.Err() == nil { @@ -992,12 +992,15 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c logger.Debug(ctx, "container ID changed, injecting subagent into new container", slog.F("old_container_id", proc.containerID), ) - recreateSubAgent = true + maybeRecreateSubAgent = true } // Container ID changed or the subagent process is not running, // stop the existing subagent context to replace it. proc.stop() + } else { + // Set SubAgent defaults. + proc.agent.OperatingSystem = "linux" // Assuming Linux for devcontainers. } // Prepare the subAgentProcess to be used when running the subagent. @@ -1090,36 +1093,29 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c // logger.Warn(ctx, "set CAP_NET_ADMIN on agent binary failed", slog.Error(err)) // } - // Detect workspace folder by executing `pwd` in the container. - // NOTE(mafredri): This is a quick and dirty way to detect the - // workspace folder inside the container. In the future we will - // rely more on `devcontainer read-configuration`. - var pwdBuf bytes.Buffer - err = api.dccli.Exec(ctx, dc.WorkspaceFolder, dc.ConfigPath, "pwd", []string{}, - WithExecOutput(&pwdBuf, io.Discard), - WithExecContainerID(container.ID), - ) - if err != nil { - return xerrors.Errorf("check workspace folder in container: %w", err) - } - directory := strings.TrimSpace(pwdBuf.String()) - if directory == "" { - logger.Warn(ctx, "detected workspace folder is empty, using default workspace folder", - slog.F("default_workspace_folder", DevcontainerDefaultContainerWorkspaceFolder), + subAgentConfig := proc.agent.CloneConfig(dc) + if proc.agent.ID == uuid.Nil || maybeRecreateSubAgent { + // Detect workspace folder by executing `pwd` in the container. + // NOTE(mafredri): This is a quick and dirty way to detect the + // workspace folder inside the container. In the future we will + // rely more on `devcontainer read-configuration`. + var pwdBuf bytes.Buffer + err = api.dccli.Exec(ctx, dc.WorkspaceFolder, dc.ConfigPath, "pwd", []string{}, + WithExecOutput(&pwdBuf, io.Discard), + WithExecContainerID(container.ID), ) - directory = DevcontainerDefaultContainerWorkspaceFolder - } - - if proc.agent.ID != uuid.Nil && recreateSubAgent { - logger.Debug(ctx, "deleting existing subagent for recreation", slog.F("agent_id", proc.agent.ID)) - client := *api.subAgentClient.Load() - err = client.Delete(ctx, proc.agent.ID) if err != nil { - return xerrors.Errorf("delete existing subagent failed: %w", err) + return xerrors.Errorf("check workspace folder in container: %w", err) } - proc.agent = SubAgent{} - } - if proc.agent.ID == uuid.Nil { + directory := strings.TrimSpace(pwdBuf.String()) + if directory == "" { + logger.Warn(ctx, "detected workspace folder is empty, using default workspace folder", + slog.F("default_workspace_folder", DevcontainerDefaultContainerWorkspaceFolder), + ) + directory = DevcontainerDefaultContainerWorkspaceFolder + } + subAgentConfig.Directory = directory + displayAppsMap := map[codersdk.DisplayApp]bool{ // NOTE(DanielleMaywood): // We use the same defaults here as set in terraform-provider-coder. @@ -1138,6 +1134,13 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c for _, customization := range coderCustomization { for app, enabled := range customization.DisplayApps { + if _, ok := displayAppsMap[app]; !ok { + logger.Warn(ctx, "unknown display app in devcontainer customization, ignoring", + slog.F("app", app), + slog.F("enabled", enabled), + ) + continue + } displayAppsMap[app] = enabled } } @@ -1149,26 +1152,41 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c displayApps = append(displayApps, app) } } + slices.Sort(displayApps) + subAgentConfig.DisplayApps = displayApps + } + + deleteSubAgent := maybeRecreateSubAgent && !proc.agent.EqualConfig(subAgentConfig) + if deleteSubAgent { + logger.Debug(ctx, "deleting existing subagent for recreation", slog.F("agent_id", proc.agent.ID)) + client := *api.subAgentClient.Load() + err = client.Delete(ctx, proc.agent.ID) + if err != nil { + return xerrors.Errorf("delete existing subagent failed: %w", err) + } + proc.agent = SubAgent{} // Clear agent to signal that we need to create a new one. + } + + if proc.agent.ID == uuid.Nil { logger.Debug(ctx, "creating new subagent", - slog.F("directory", directory), - slog.F("display_apps", displayApps), + slog.F("directory", subAgentConfig.Directory), + slog.F("display_apps", subAgentConfig.DisplayApps), ) // Create new subagent record in the database to receive the auth token. client := *api.subAgentClient.Load() - proc.agent, err = client.Create(ctx, SubAgent{ - Name: dc.Name, - Directory: directory, - OperatingSystem: "linux", // Assuming Linux for devcontainers. - Architecture: arch, - DisplayApps: displayApps, - }) + newSubAgent, err := client.Create(ctx, subAgentConfig) if err != nil { return xerrors.Errorf("create subagent failed: %w", err) } + proc.agent = newSubAgent logger.Info(ctx, "created new subagent", slog.F("agent_id", proc.agent.ID)) + } else { + logger.Debug(ctx, "subagent already exists, skipping recreation", + slog.F("agent_id", proc.agent.ID), + ) } api.mu.Lock() // Re-lock to update the agent. diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index 92a697b6e23b4..e50324ab2a7ae 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -212,6 +212,7 @@ func (w *fakeWatcher) sendEventWaitNextCalled(ctx context.Context, event fsnotif // fakeSubAgentClient implements SubAgentClient for testing purposes. type fakeSubAgentClient struct { + logger slog.Logger agents map[uuid.UUID]agentcontainers.SubAgent listErrC chan error // If set, send to return error, close to return nil. @@ -240,6 +241,7 @@ func (m *fakeSubAgentClient) List(ctx context.Context) ([]agentcontainers.SubAge } func (m *fakeSubAgentClient) Create(ctx context.Context, agent agentcontainers.SubAgent) (agentcontainers.SubAgent, error) { + m.logger.Debug(ctx, "creating sub agent", slog.F("agent", agent)) if m.createErrC != nil { select { case <-ctx.Done(): @@ -261,6 +263,7 @@ func (m *fakeSubAgentClient) Create(ctx context.Context, agent agentcontainers.S } func (m *fakeSubAgentClient) Delete(ctx context.Context, id uuid.UUID) error { + m.logger.Debug(ctx, "deleting sub agent", slog.F("id", id.String())) if m.deleteErrC != nil { select { case <-ctx.Done(): @@ -1245,6 +1248,7 @@ func TestAPI(t *testing.T) { mClock = quartz.NewMock(t) mCCLI = acmock.NewMockContainerCLI(gomock.NewController(t)) fakeSAC = &fakeSubAgentClient{ + logger: logger.Named("fakeSubAgentClient"), createErrC: make(chan error, 1), deleteErrC: make(chan error, 1), } @@ -1270,7 +1274,7 @@ func TestAPI(t *testing.T) { mCCLI.EXPECT().List(gomock.Any()).Return(codersdk.WorkspaceAgentListContainersResponse{ Containers: []codersdk.WorkspaceAgentContainer{testContainer}, - }, nil).Times(1 + 3) // 1 initial call + 3 updates. + }, nil).Times(3) // 1 initial call + 2 updates. gomock.InOrder( mCCLI.EXPECT().DetectArchitecture(gomock.Any(), "test-container-id").Return(runtime.GOARCH, nil), mCCLI.EXPECT().ExecAs(gomock.Any(), "test-container-id", "root", "mkdir", "-p", "/.coder-agent").Return(nil, nil), @@ -1315,19 +1319,20 @@ func TestAPI(t *testing.T) { tickerTrap.MustWait(ctx).MustRelease(ctx) tickerTrap.Close() - // Ensure we only inject the agent once. - for i := range 3 { - _, aw := mClock.AdvanceNext() - aw.MustWait(ctx) + // Refresh twice to ensure idempotency of agent creation. + err = api.RefreshContainers(ctx) + require.NoError(t, err, "refresh containers should not fail") + t.Logf("Agents created: %d, deleted: %d", len(fakeSAC.created), len(fakeSAC.deleted)) - t.Logf("Iteration %d: agents created: %d", i+1, len(fakeSAC.created)) + err = api.RefreshContainers(ctx) + require.NoError(t, err, "refresh containers should not fail") + t.Logf("Agents created: %d, deleted: %d", len(fakeSAC.created), len(fakeSAC.deleted)) - // Verify agent was created. - require.Len(t, fakeSAC.created, 1) - assert.Equal(t, "test-container", fakeSAC.created[0].Name) - assert.Equal(t, "/workspaces", fakeSAC.created[0].Directory) - assert.Len(t, fakeSAC.deleted, 0) - } + // Verify agent was created. + require.Len(t, fakeSAC.created, 1) + assert.Equal(t, "test-container", fakeSAC.created[0].Name) + assert.Equal(t, "/workspaces", fakeSAC.created[0].Directory) + assert.Len(t, fakeSAC.deleted, 0) t.Log("Agent injected successfully, now testing reinjection into the same container...") @@ -1349,7 +1354,7 @@ func TestAPI(t *testing.T) { // Expect the agent to be reinjected. mCCLI.EXPECT().List(gomock.Any()).Return(codersdk.WorkspaceAgentListContainersResponse{ Containers: []codersdk.WorkspaceAgentContainer{testContainer}, - }, nil).Times(3) // 3 updates. + }, nil).Times(1) // 1 update. gomock.InOrder( mCCLI.EXPECT().DetectArchitecture(gomock.Any(), "test-container-id").Return(runtime.GOARCH, nil), mCCLI.EXPECT().ExecAs(gomock.Any(), "test-container-id", "root", "mkdir", "-p", "/.coder-agent").Return(nil, nil), @@ -1357,24 +1362,15 @@ func TestAPI(t *testing.T) { mCCLI.EXPECT().ExecAs(gomock.Any(), "test-container-id", "root", "chmod", "0755", "/.coder-agent", "/.coder-agent/coder").Return(nil, nil), ) - // Allow agent reinjection to succeed. - testutil.RequireSend(ctx, t, fakeDCCLI.execErrC, func(cmd string, args ...string) error { - assert.Equal(t, "pwd", cmd) - assert.Empty(t, args) - return nil - }) // Exec pwd. - - // Ensure we only inject the agent once. - for i := range 3 { - _, aw := mClock.AdvanceNext() - aw.MustWait(ctx) + // Agent reinjection will succeed and we will not re-create the + // agent, nor re-probe pwd. + err = api.RefreshContainers(ctx) + require.NoError(t, err, "refresh containers should not fail") + t.Logf("Agents created: %d, deleted: %d", len(fakeSAC.created), len(fakeSAC.deleted)) - t.Logf("Iteration %d: agents created: %d", i+1, len(fakeSAC.created)) - - // Verify that the agent was reused. - require.Len(t, fakeSAC.created, 1) - assert.Len(t, fakeSAC.deleted, 0) - } + // Verify that the agent was reused. + require.Len(t, fakeSAC.created, 1) + assert.Len(t, fakeSAC.deleted, 0) t.Log("Agent reinjected successfully, now testing agent deletion and recreation...") @@ -1383,7 +1379,7 @@ func TestAPI(t *testing.T) { // Expect the agent to be injected. mCCLI.EXPECT().List(gomock.Any()).Return(codersdk.WorkspaceAgentListContainersResponse{ Containers: []codersdk.WorkspaceAgentContainer{testContainer}, - }, nil).Times(3) // 3 updates. + }, nil).Times(1) // 1 update. gomock.InOrder( mCCLI.EXPECT().DetectArchitecture(gomock.Any(), "new-test-container-id").Return(runtime.GOARCH, nil), mCCLI.EXPECT().ExecAs(gomock.Any(), "new-test-container-id", "root", "mkdir", "-p", "/.coder-agent").Return(nil, nil), @@ -1404,7 +1400,20 @@ func TestAPI(t *testing.T) { }) <-terminated - // Simulate the agent deletion. + fakeDCCLI.readConfig.MergedConfiguration.Customizations.Coder = []agentcontainers.CoderCustomization{ + { + DisplayApps: map[codersdk.DisplayApp]bool{ + codersdk.DisplayAppSSH: true, + codersdk.DisplayAppWebTerminal: true, + codersdk.DisplayAppVSCodeDesktop: true, + codersdk.DisplayAppVSCodeInsiders: true, + codersdk.DisplayAppPortForward: true, + }, + }, + } + + // Simulate the agent deletion (this happens because the + // devcontainer configuration changed). testutil.RequireSend(ctx, t, fakeSAC.deleteErrC, nil) // Expect the agent to be recreated. testutil.RequireSend(ctx, t, fakeSAC.createErrC, nil) @@ -1414,13 +1423,9 @@ func TestAPI(t *testing.T) { return nil }) // Exec pwd. - // Advance the clock to run updaterLoop. - for i := range 3 { - _, aw := mClock.AdvanceNext() - aw.MustWait(ctx) - - t.Logf("Iteration %d: agents created: %d, deleted: %d", i+1, len(fakeSAC.created), len(fakeSAC.deleted)) - } + err = api.RefreshContainers(ctx) + require.NoError(t, err, "refresh containers should not fail") + t.Logf("Agents created: %d, deleted: %d", len(fakeSAC.created), len(fakeSAC.deleted)) // Verify the agent was deleted and recreated. require.Len(t, fakeSAC.deleted, 1, "there should be one deleted agent after recreation") @@ -1453,6 +1458,7 @@ func TestAPI(t *testing.T) { mClock = quartz.NewMock(t) mCCLI = acmock.NewMockContainerCLI(gomock.NewController(t)) fakeSAC = &fakeSubAgentClient{ + logger: logger.Named("fakeSubAgentClient"), agents: map[uuid.UUID]agentcontainers.SubAgent{ existingAgentID: existingAgent, }, @@ -1577,7 +1583,10 @@ func TestAPI(t *testing.T) { logger = testutil.Logger(t) mClock = quartz.NewMock(t) mCCLI = acmock.NewMockContainerCLI(gomock.NewController(t)) - fSAC = &fakeSubAgentClient{createErrC: make(chan error, 1)} + fSAC = &fakeSubAgentClient{ + logger: logger.Named("fakeSubAgentClient"), + createErrC: make(chan error, 1), + } fDCCLI = &fakeDevcontainerCLI{ readConfig: agentcontainers.DevcontainerConfig{ MergedConfiguration: agentcontainers.DevcontainerConfiguration{ diff --git a/agent/agentcontainers/subagent.go b/agent/agentcontainers/subagent.go index 5848e5747e099..ea527f8c46e37 100644 --- a/agent/agentcontainers/subagent.go +++ b/agent/agentcontainers/subagent.go @@ -2,6 +2,7 @@ package agentcontainers import ( "context" + "slices" "github.com/google/uuid" "golang.org/x/xerrors" @@ -23,6 +24,26 @@ type SubAgent struct { DisplayApps []codersdk.DisplayApp } +// CloneConfig makes a copy of SubAgent without ID and AuthToken. The +// name is inherited from the devcontainer. +func (s SubAgent) CloneConfig(dc codersdk.WorkspaceAgentDevcontainer) SubAgent { + return SubAgent{ + Name: dc.Name, + Directory: s.Directory, + Architecture: s.Architecture, + OperatingSystem: s.OperatingSystem, + DisplayApps: slices.Clone(s.DisplayApps), + } +} + +func (s SubAgent) EqualConfig(other SubAgent) bool { + return s.Name == other.Name && + s.Directory == other.Directory && + s.Architecture == other.Architecture && + s.OperatingSystem == other.OperatingSystem && + slices.Equal(s.DisplayApps, other.DisplayApps) +} + // SubAgentClient is an interface for managing sub agents and allows // changing the implementation without having to deal with the // agentproto package directly. From 6aa8eafd1e01fd641df6548f29650d57648a5064 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 17 Jun 2025 12:51:32 +0000 Subject: [PATCH 2/5] fix whoopsie --- agent/agentcontainers/api.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index 40a4999f0c2c7..93c209017f03a 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -992,7 +992,7 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c logger.Debug(ctx, "container ID changed, injecting subagent into new container", slog.F("old_container_id", proc.containerID), ) - maybeRecreateSubAgent = true + maybeRecreateSubAgent = proc.agent.ID != uuid.Nil } // Container ID changed or the subagent process is not running, @@ -1157,7 +1157,7 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c subAgentConfig.DisplayApps = displayApps } - deleteSubAgent := maybeRecreateSubAgent && !proc.agent.EqualConfig(subAgentConfig) + deleteSubAgent := proc.agent.ID != uuid.Nil && maybeRecreateSubAgent && !proc.agent.EqualConfig(subAgentConfig) if deleteSubAgent { logger.Debug(ctx, "deleting existing subagent for recreation", slog.F("agent_id", proc.agent.ID)) client := *api.subAgentClient.Load() From 6081a2ecc2ae1b22cb4c6aacd93a13b1784d49d8 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 17 Jun 2025 13:22:01 +0000 Subject: [PATCH 3/5] avoid resetting agent since its often kept --- agent/agentcontainers/api.go | 4 ++-- site/src/modules/resources/AgentDevcontainerCard.tsx | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index 93c209017f03a..785d87bf3654e 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -671,9 +671,9 @@ func (api *API) getContainers() (codersdk.WorkspaceAgentListContainersResponse, if len(api.knownDevcontainers) > 0 { devcontainers = make([]codersdk.WorkspaceAgentDevcontainer, 0, len(api.knownDevcontainers)) for _, dc := range api.knownDevcontainers { - // Include the agent if it's been created (we're iterating over + // Include the agent if it's running (we're iterating over // copies, so mutating is fine). - if proc := api.injectedSubAgentProcs[dc.WorkspaceFolder]; proc.agent.ID != uuid.Nil && dc.Container != nil && proc.containerID == dc.Container.ID { + if proc := api.injectedSubAgentProcs[dc.WorkspaceFolder]; proc.agent.ID != uuid.Nil { dc.Agent = &codersdk.WorkspaceAgentDevcontainerAgent{ ID: proc.agent.ID, Name: proc.agent.Name, diff --git a/site/src/modules/resources/AgentDevcontainerCard.tsx b/site/src/modules/resources/AgentDevcontainerCard.tsx index 9ba6e26c5d46a..9985b03f2718d 100644 --- a/site/src/modules/resources/AgentDevcontainerCard.tsx +++ b/site/src/modules/resources/AgentDevcontainerCard.tsx @@ -116,7 +116,6 @@ export const AgentDevcontainerCard: FC = ({ if (dc.id === devcontainer.id) { return { ...dc, - agent: null, container: null, status: "starting", }; From 43d1a860f5f9b1346ceb59b3d84858e48f5306df Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 17 Jun 2025 13:45:35 +0000 Subject: [PATCH 4/5] fix test --- agent/agentcontainers/api_test.go | 63 ++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index e50324ab2a7ae..71321b3b0fbae 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -1347,14 +1347,11 @@ func TestAPI(t *testing.T) { } return errTestTermination }) - <-terminated + testutil.RequireReceive(ctx, t, terminated) t.Log("Waiting for agent reinjection...") // Expect the agent to be reinjected. - mCCLI.EXPECT().List(gomock.Any()).Return(codersdk.WorkspaceAgentListContainersResponse{ - Containers: []codersdk.WorkspaceAgentContainer{testContainer}, - }, nil).Times(1) // 1 update. gomock.InOrder( mCCLI.EXPECT().DetectArchitecture(gomock.Any(), "test-container-id").Return(runtime.GOARCH, nil), mCCLI.EXPECT().ExecAs(gomock.Any(), "test-container-id", "root", "mkdir", "-p", "/.coder-agent").Return(nil, nil), @@ -1362,11 +1359,42 @@ func TestAPI(t *testing.T) { mCCLI.EXPECT().ExecAs(gomock.Any(), "test-container-id", "root", "chmod", "0755", "/.coder-agent", "/.coder-agent/coder").Return(nil, nil), ) - // Agent reinjection will succeed and we will not re-create the - // agent, nor re-probe pwd. - err = api.RefreshContainers(ctx) - require.NoError(t, err, "refresh containers should not fail") - t.Logf("Agents created: %d, deleted: %d", len(fakeSAC.created), len(fakeSAC.deleted)) + // Verify that the agent has started. + agentStarted := make(chan struct{}) + continueTerminate := make(chan struct{}) + terminated = make(chan struct{}) + testutil.RequireSend(ctx, t, fakeDCCLI.execErrC, func(_ string, args ...string) error { + defer close(terminated) + if len(args) > 0 { + assert.Equal(t, "agent", args[0]) + } else { + assert.Fail(t, `want "agent" command argument`) + } + close(agentStarted) + <-continueTerminate + return errTestTermination + }) + + WaitStartLoop: + for { + // Agent reinjection will succeed and we will not re-create the + // agent, nor re-probe pwd. + mCCLI.EXPECT().List(gomock.Any()).Return(codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{testContainer}, + }, nil).Times(1) // 1 update. + err = api.RefreshContainers(ctx) + require.NoError(t, err, "refresh containers should not fail") + + t.Logf("Agents created: %d, deleted: %d", len(fakeSAC.created), len(fakeSAC.deleted)) + + select { + case <-agentStarted: + break WaitStartLoop + case <-ctx.Done(): + t.Fatal("timeout waiting for agent to start") + default: + } + } // Verify that the agent was reused. require.Len(t, fakeSAC.created, 1) @@ -1387,19 +1415,6 @@ func TestAPI(t *testing.T) { mCCLI.EXPECT().ExecAs(gomock.Any(), "new-test-container-id", "root", "chmod", "0755", "/.coder-agent", "/.coder-agent/coder").Return(nil, nil), ) - // Terminate the agent and verify it can be reinjected. - terminated = make(chan struct{}) - testutil.RequireSend(ctx, t, fakeDCCLI.execErrC, func(_ string, args ...string) error { - defer close(terminated) - if len(args) > 0 { - assert.Equal(t, "agent", args[0]) - } else { - assert.Fail(t, `want "agent" command argument`) - } - return errTestTermination - }) - <-terminated - fakeDCCLI.readConfig.MergedConfiguration.Customizations.Coder = []agentcontainers.CoderCustomization{ { DisplayApps: map[codersdk.DisplayApp]bool{ @@ -1412,6 +1427,10 @@ func TestAPI(t *testing.T) { }, } + // Terminate the running agent. + close(continueTerminate) + testutil.RequireReceive(ctx, t, terminated) + // Simulate the agent deletion (this happens because the // devcontainer configuration changed). testutil.RequireSend(ctx, t, fakeSAC.deleteErrC, nil) From b861e04a0d3511688c39b4992ccfad7e820530bf Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 17 Jun 2025 14:16:29 +0000 Subject: [PATCH 5/5] requirereceive bad mumbo jumbo --- agent/agentcontainers/api_test.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index 71321b3b0fbae..8dc1f83dc916b 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -1347,7 +1347,11 @@ func TestAPI(t *testing.T) { } return errTestTermination }) - testutil.RequireReceive(ctx, t, terminated) + select { + case <-ctx.Done(): + t.Fatal("timeout waiting for agent termination") + case <-terminated: + } t.Log("Waiting for agent reinjection...") @@ -1371,7 +1375,11 @@ func TestAPI(t *testing.T) { assert.Fail(t, `want "agent" command argument`) } close(agentStarted) - <-continueTerminate + select { + case <-ctx.Done(): + t.Error("timeout waiting for agent continueTerminate") + case <-continueTerminate: + } return errTestTermination }) @@ -1429,7 +1437,11 @@ func TestAPI(t *testing.T) { // Terminate the running agent. close(continueTerminate) - testutil.RequireReceive(ctx, t, terminated) + select { + case <-ctx.Done(): + t.Fatal("timeout waiting for agent termination") + case <-terminated: + } // Simulate the agent deletion (this happens because the // devcontainer configuration changed). pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy