fix: gate chat advisor and virtual desktop behind experiments, delete experiments page (#26809)

This commit is contained in:
Danielle Maywood
2026-06-30 21:57:40 +01:00
committed by GitHub
parent dcb120d6ab
commit 6b8c38b5a4
43 changed files with 907 additions and 2110 deletions
+11 -3
View File
@@ -19630,12 +19630,16 @@ const docTemplate = `{
"nats_pubsub",
"minimum-implicit-member",
"ai-gateway-cost-control",
"agent-app-tabs"
"agent-app-tabs",
"chat-advisor",
"chat-virtual-desktop"
],
"x-enum-comments": {
"ExperimentAIGatewayCostControl": "Enables AI Gateway cost control functionality.",
"ExperimentAgentAppTabs": "Enables workspace-app and port preview tabs in the Coder Agents right panel.",
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
"ExperimentChatAdvisor": "Enables the advisor tool for root agent chats.",
"ExperimentChatVirtualDesktop": "Enables virtual desktop and computer use provider for agents.",
"ExperimentExample": "This isn't used for anything.",
"ExperimentMCPServerHTTP": "Enables the MCP HTTP server functionality.",
"ExperimentMinimumImplicitMember": "Allows organizations to deviate from the default organization-member roles, in support of Gateway Accounts.",
@@ -19656,7 +19660,9 @@ const docTemplate = `{
"Enables embedded NATS pubsub.",
"Allows organizations to deviate from the default organization-member roles, in support of Gateway Accounts.",
"Enables AI Gateway cost control functionality.",
"Enables workspace-app and port preview tabs in the Coder Agents right panel."
"Enables workspace-app and port preview tabs in the Coder Agents right panel.",
"Enables the advisor tool for root agent chats.",
"Enables virtual desktop and computer use provider for agents."
],
"x-enum-varnames": [
"ExperimentExample",
@@ -19669,7 +19675,9 @@ const docTemplate = `{
"ExperimentNATSPubsub",
"ExperimentMinimumImplicitMember",
"ExperimentAIGatewayCostControl",
"ExperimentAgentAppTabs"
"ExperimentAgentAppTabs",
"ExperimentChatAdvisor",
"ExperimentChatVirtualDesktop"
]
},
"codersdk.ExternalAPIKeyScopes": {
+11 -3
View File
@@ -17823,12 +17823,16 @@
"nats_pubsub",
"minimum-implicit-member",
"ai-gateway-cost-control",
"agent-app-tabs"
"agent-app-tabs",
"chat-advisor",
"chat-virtual-desktop"
],
"x-enum-comments": {
"ExperimentAIGatewayCostControl": "Enables AI Gateway cost control functionality.",
"ExperimentAgentAppTabs": "Enables workspace-app and port preview tabs in the Coder Agents right panel.",
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
"ExperimentChatAdvisor": "Enables the advisor tool for root agent chats.",
"ExperimentChatVirtualDesktop": "Enables virtual desktop and computer use provider for agents.",
"ExperimentExample": "This isn't used for anything.",
"ExperimentMCPServerHTTP": "Enables the MCP HTTP server functionality.",
"ExperimentMinimumImplicitMember": "Allows organizations to deviate from the default organization-member roles, in support of Gateway Accounts.",
@@ -17849,7 +17853,9 @@
"Enables embedded NATS pubsub.",
"Allows organizations to deviate from the default organization-member roles, in support of Gateway Accounts.",
"Enables AI Gateway cost control functionality.",
"Enables workspace-app and port preview tabs in the Coder Agents right panel."
"Enables workspace-app and port preview tabs in the Coder Agents right panel.",
"Enables the advisor tool for root agent chats.",
"Enables virtual desktop and computer use provider for agents."
],
"x-enum-varnames": [
"ExperimentExample",
@@ -17862,7 +17868,9 @@
"ExperimentNATSPubsub",
"ExperimentMinimumImplicitMember",
"ExperimentAIGatewayCostControl",
"ExperimentAgentAppTabs"
"ExperimentAgentAppTabs",
"ExperimentChatAdvisor",
"ExperimentChatVirtualDesktop"
]
},
"codersdk.ExternalAPIKeyScopes": {
+11 -6
View File
@@ -840,6 +840,7 @@ func New(options *Options) *API {
AIBridgeTransportFactory: &api.AIBridgeTransportFactory,
AIGatewayRoutingEnabled: chatAIGatewayRoutingEnabled,
AlwaysEnableDebugLogs: options.DeploymentValues.AI.Chat.DebugLoggingEnabled.Value(),
Experiments: experiments,
AgentConn: api.agentProvider.AgentConn,
AgentInactiveDisconnectTimeout: api.AgentInactiveDisconnectTimeout,
InstructionLookupTimeout: options.ChatdInstructionLookupTimeout,
@@ -1279,16 +1280,20 @@ func New(options *Options) *API {
r.Put("/personal-model-overrides", api.putChatPersonalModelOverridesAdminSettings)
r.Get("/user-personal-model-overrides", api.getUserChatPersonalModelOverrides)
r.Put("/user-personal-model-overrides/{context}", api.putUserChatPersonalModelOverride)
r.Get("/desktop-enabled", api.getChatDesktopEnabled)
r.Put("/desktop-enabled", api.putChatDesktopEnabled)
r.Get("/computer-use-provider", api.getChatComputerUseProvider)
r.Put("/computer-use-provider", api.putChatComputerUseProvider)
r.Group(func(r chi.Router) {
r.Use(httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentChatVirtualDesktop))
r.Get("/computer-use-provider", api.getChatComputerUseProvider)
r.Put("/computer-use-provider", api.putChatComputerUseProvider)
})
r.Get("/debug-logging", api.getChatDebugLogging)
r.Put("/debug-logging", api.putChatDebugLogging)
r.Get("/user-debug-logging", api.getUserChatDebugLogging)
r.Put("/user-debug-logging", api.putUserChatDebugLogging)
r.Get("/advisor", api.getChatAdvisorConfig)
r.Put("/advisor", api.putChatAdvisorConfig)
r.Group(func(r chi.Router) {
r.Use(httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentChatAdvisor))
r.Get("/advisor", api.getChatAdvisorConfig)
r.Put("/advisor", api.putChatAdvisorConfig)
})
r.Get("/user-prompt", api.getUserChatCustomPrompt)
r.Put("/user-prompt", api.putUserChatCustomPrompt)
r.Get("/user-compaction-thresholds", api.getUserChatCompactionThresholds)
+1 -43
View File
@@ -5176,49 +5176,6 @@ func (api *API) putUserChatPersonalModelOverride(rw http.ResponseWriter, r *http
rw.WriteHeader(http.StatusNoContent)
}
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
//
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
func (api *API) getChatDesktopEnabled(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
enabled, err := api.Database.GetChatDesktopEnabled(ctx)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching desktop setting.",
Detail: err.Error(),
})
return
}
httpapi.Write(ctx, rw, http.StatusOK, codersdk.ChatDesktopEnabledResponse{
EnableDesktop: enabled,
})
}
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
func (api *API) putChatDesktopEnabled(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceDeploymentConfig) {
httpapi.Forbidden(rw)
return
}
var req codersdk.UpdateChatDesktopEnabledRequest
if !httpapi.Read(ctx, rw, r, &req) {
return
}
if err := api.Database.UpsertChatDesktopEnabled(ctx, req.EnableDesktop); httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
} else if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error updating desktop setting.",
Detail: err.Error(),
})
return
}
rw.WriteHeader(http.StatusNoContent)
}
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
//
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
@@ -5429,6 +5386,7 @@ func (api *API) getChatAdvisorConfig(rw http.ResponseWriter, r *http.Request) {
}
resp.MaxUsesPerRun = max(resp.MaxUsesPerRun, 0)
resp.MaxOutputTokens = max(resp.MaxOutputTokens, 0)
resp.Enabled = api.Experiments.Enabled(codersdk.ExperimentChatAdvisor)
httpapi.Write(ctx, rw, http.StatusOK, resp)
}
+21 -111
View File
@@ -69,6 +69,14 @@ func newChatTestOptions(
) *coderdtest.Options {
t.Helper()
// Enable experiment-gated chat endpoints in tests.
if len(values.Experiments) == 0 {
values.Experiments = serpent.StringArray{
string(codersdk.ExperimentChatAdvisor),
string(codersdk.ExperimentChatVirtualDesktop),
}
}
opts := &coderdtest.Options{
DeploymentValues: values,
}
@@ -12270,110 +12278,6 @@ func TestCreateChatPersonalModelOverrideRoot(t *testing.T) {
})
}
func TestChatDesktopEnabled(t *testing.T) {
t.Parallel()
t.Run("ReturnsFalseWhenUnset", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
adminClient := newChatClient(t)
coderdtest.CreateFirstUser(t, adminClient.Client)
resp, err := adminClient.GetChatDesktopEnabled(ctx)
require.NoError(t, err)
require.False(t, resp.EnableDesktop)
})
t.Run("AdminCanSetTrue", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
adminClient := newChatClient(t)
coderdtest.CreateFirstUser(t, adminClient.Client)
err := adminClient.UpdateChatDesktopEnabled(ctx, codersdk.UpdateChatDesktopEnabledRequest{
EnableDesktop: true,
})
require.NoError(t, err)
resp, err := adminClient.GetChatDesktopEnabled(ctx)
require.NoError(t, err)
require.True(t, resp.EnableDesktop)
})
t.Run("AdminCanSetFalse", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
adminClient := newChatClient(t)
coderdtest.CreateFirstUser(t, adminClient.Client)
// Set true first, then set false.
err := adminClient.UpdateChatDesktopEnabled(ctx, codersdk.UpdateChatDesktopEnabledRequest{
EnableDesktop: true,
})
require.NoError(t, err)
err = adminClient.UpdateChatDesktopEnabled(ctx, codersdk.UpdateChatDesktopEnabledRequest{
EnableDesktop: false,
})
require.NoError(t, err)
resp, err := adminClient.GetChatDesktopEnabled(ctx)
require.NoError(t, err)
require.False(t, resp.EnableDesktop)
})
t.Run("NonAdminCanRead", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
adminClient := newChatClient(t)
firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
memberClient := codersdk.NewExperimentalClient(memberClientRaw)
err := adminClient.UpdateChatDesktopEnabled(ctx, codersdk.UpdateChatDesktopEnabledRequest{
EnableDesktop: true,
})
require.NoError(t, err)
resp, err := memberClient.GetChatDesktopEnabled(ctx)
require.NoError(t, err)
require.True(t, resp.EnableDesktop)
})
t.Run("NonAdminWriteFails", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
adminClient := newChatClient(t)
firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
memberClient := codersdk.NewExperimentalClient(memberClientRaw)
err := memberClient.UpdateChatDesktopEnabled(ctx, codersdk.UpdateChatDesktopEnabledRequest{
EnableDesktop: true,
})
requireSDKError(t, err, http.StatusForbidden)
})
t.Run("UnauthenticatedFails", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
adminClient := newChatClient(t)
coderdtest.CreateFirstUser(t, adminClient.Client)
anonClient := codersdk.NewExperimentalClient(codersdk.New(adminClient.URL))
_, err := anonClient.GetChatDesktopEnabled(ctx)
var sdkErr *codersdk.Error
require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusUnauthorized, sdkErr.StatusCode())
})
}
func TestChatComputerUseProvider(t *testing.T) {
t.Parallel()
@@ -13000,7 +12904,12 @@ func TestChatAdvisorConfig_GetDefault(t *testing.T) {
resp, err := adminClient.GetChatAdvisorConfig(ctx)
require.NoError(t, err)
require.Equal(t, codersdk.AdvisorConfig{}, resp)
// Enabled reflects the experiment state, not the DB value. The test
// deployment enables chat-advisor, so Enabled is true.
require.True(t, resp.Enabled)
require.Equal(t, 0, resp.MaxUsesPerRun)
require.Equal(t, int64(0), resp.MaxOutputTokens)
require.Equal(t, uuid.Nil, resp.ModelConfigID)
}
func TestChatAdvisorConfig_Update(t *testing.T) {
@@ -13189,11 +13098,11 @@ func TestChatAdvisorConfig_OverwriteClearsPreviousValues(t *testing.T) {
require.Equal(t, sparse, resp)
}
// TestChatAdvisorConfig_CanBeDisabledAfterEnabled pins the feature
// gate's "off" path. The downstream runtime gates the advisor tool and
// prompt guidance on Enabled, so a regression that silently drops or
// ignores Enabled: false on PUT would leave the feature stuck on.
func TestChatAdvisorConfig_CanBeDisabledAfterEnabled(t *testing.T) {
// TestChatAdvisorConfig_EnabledReflectsExperiment pins that the Enabled
// field in the GET response reflects the experiment state, not the DB-stored
// value. Setting Enabled: false via PUT stores false in the DB, but the GET
// handler overrides it with the experiment check.
func TestChatAdvisorConfig_EnabledReflectsExperiment(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
@@ -13217,7 +13126,8 @@ func TestChatAdvisorConfig_CanBeDisabledAfterEnabled(t *testing.T) {
disabledResp, err := adminClient.GetChatAdvisorConfig(ctx)
require.NoError(t, err)
require.False(t, disabledResp.Enabled)
// Enabled reflects the experiment state (on), not the DB-stored false.
require.True(t, disabledResp.Enabled)
}
func TestChatAdvisorConfig_ClampsNegativeStoredValues(t *testing.T) {
+3
View File
@@ -190,6 +190,7 @@ type Server struct {
aibridgeTransportFactory *atomic.Pointer[aibridge.TransportFactory]
aiGatewayRoutingEnabled bool
experiments codersdk.Experiments
// Configuration
pendingChatAcquireInterval time.Duration
@@ -3166,6 +3167,7 @@ type Config struct {
Clock quartz.Clock
AIBridgeTransportFactory *atomic.Pointer[aibridge.TransportFactory]
AIGatewayRoutingEnabled bool
Experiments codersdk.Experiments
PrometheusRegistry prometheus.Registerer
@@ -3262,6 +3264,7 @@ func New(ps pubsub.Pubsub, cfg Config) *Server {
},
aibridgeTransportFactory: cfg.AIBridgeTransportFactory,
aiGatewayRoutingEnabled: cfg.AIGatewayRoutingEnabled,
experiments: cfg.Experiments,
pendingChatAcquireInterval: pendingChatAcquireInterval,
maxChatsPerAcquire: maxChatsPerAcquire,
inFlightChatStaleAfter: inFlightChatStaleAfter,
+39 -31
View File
@@ -5240,6 +5240,7 @@ func newTestServer(
Database: db,
ReplicaID: replicaID,
PendingChatAcquireInterval: testutil.WaitLong,
Experiments: codersdk.ExperimentsKnown,
})
t.Cleanup(func() {
require.NoError(t, server.Close())
@@ -8135,6 +8136,7 @@ func newActiveTestServer(
ReplicaID: uuid.New(),
PendingChatAcquireInterval: 10 * time.Millisecond,
InFlightChatStaleAfter: testutil.WaitSuperLong,
Experiments: codersdk.ExperimentsKnown,
}
for _, o := range overrides {
o(&cfg)
@@ -9319,9 +9321,6 @@ func TestComputerUseSubagentToolsAndModel(t *testing.T) {
BaseUrl: anthropicSrv.URL,
})
err := db.UpsertChatDesktopEnabled(ctx, true)
require.NoError(t, err)
// Build workspace + agent records so getWorkspaceConn can
// resolve the agent for the computer use child.
ws, dbAgent := seedWorkspaceWithAgent(t, db, user.ID)
@@ -9514,6 +9513,7 @@ func TestInterruptChatPersistsPartialResponse(t *testing.T) {
ReplicaID: uuid.New(),
PendingChatAcquireInterval: 10 * time.Millisecond,
InFlightChatStaleAfter: testutil.WaitSuperLong,
Experiments: codersdk.ExperimentsKnown,
})
server.Start()
t.Cleanup(func() {
@@ -11602,51 +11602,61 @@ func TestAcquireChatsSkipsArchivedPendingChat(t *testing.T) {
require.Equal(t, activeChat.ID, acquired[0].ID)
}
func TestAdvisorGating_Disabled(t *testing.T) {
// TestAdvisorGating_ExperimentDisabled verifies that the advisor tool is
// not attached when the chat-advisor experiment is absent from the
// experiments list, even if the DB-stored advisor config has Enabled=true.
func TestAdvisorGating_ExperimentDisabled(t *testing.T) {
t.Parallel()
db, ps := dbtestutil.NewDB(t)
ctx := testutil.Context(t, testutil.WaitLong)
var toolsMu sync.Mutex
var capturedTools []string
var capturedMessages []chattest.OpenAIMessage
var streamedCallCount atomic.Int32
var streamedCallsMu sync.Mutex
var firstCallTools []string
openAIURL := chattest.NewOpenAI(t, func(req *chattest.OpenAIRequest) chattest.OpenAIResponse {
if !req.Stream {
return chattest.OpenAINonStreamingResponse("title")
}
names := make([]string, 0, len(req.Tools))
for _, tool := range req.Tools {
names = append(names, tool.Function.Name)
if streamedCallCount.Add(1) == 1 {
names := make([]string, 0, len(req.Tools))
for _, tool := range req.Tools {
names = append(names, tool.Function.Name)
}
streamedCallsMu.Lock()
firstCallTools = names
streamedCallsMu.Unlock()
}
toolsMu.Lock()
capturedTools = names
capturedMessages = append([]chattest.OpenAIMessage(nil), req.Messages...)
toolsMu.Unlock()
return chattest.OpenAIStreamingResponse(
chattest.OpenAITextChunks("advisor is not available")...,
chattest.OpenAITextChunks("done")...,
)
})
user, org, model := seedChatDependenciesWithProvider(t, db, "openai-compat", openAIURL)
seedAdvisorConfig(ctx, t, db, codersdk.AdvisorConfig{
Enabled: false,
Enabled: true,
MaxUsesPerRun: 3,
MaxOutputTokens: 16384,
})
server := newActiveTestServer(t, db, ps)
experiments := slices.DeleteFunc(
slices.Clone(codersdk.ExperimentsKnown),
func(e codersdk.Experiment) bool { return e == codersdk.ExperimentChatAdvisor },
)
server := newActiveTestServer(t, db, ps, func(cfg *chatd.Config) {
cfg.Experiments = experiments
})
chat, err := server.CreateChat(ctx, chatd.CreateOptions{
OrganizationID: org.ID,
OwnerID: user.ID,
APIKeyID: testAPIKeyID(t, db, user.ID),
Title: "advisor-disabled",
Title: "advisor-experiment-disabled",
ModelConfigID: model.ID,
InitialUserContent: []codersdk.ChatMessagePart{
codersdk.ChatMessageText("hello"),
codersdk.ChatMessageText("help me plan this"),
},
})
require.NoError(t, err)
@@ -11656,22 +11666,20 @@ func TestAdvisorGating_Disabled(t *testing.T) {
if getErr != nil {
return false
}
return got.Status == database.ChatStatusWaiting ||
got.Status == database.ChatStatusError
if got.Status != database.ChatStatusWaiting &&
got.Status != database.ChatStatusError {
return false
}
return streamedCallCount.Load() >= 1
}, testutil.WaitLong, testutil.IntervalFast)
toolsMu.Lock()
tools := append([]string(nil), capturedTools...)
messages := append([]chattest.OpenAIMessage(nil), capturedMessages...)
toolsMu.Unlock()
streamedCallsMu.Lock()
tools := append([]string(nil), firstCallTools...)
streamedCallsMu.Unlock()
require.NotEmpty(t, messages, "expected a streamed LLM request")
require.NotEmpty(t, tools, "expected at least one streamed LLM request")
require.NotContains(t, tools, "advisor",
"advisor tool should not be registered when disabled")
for _, msg := range messages {
require.NotContains(t, msg.Content, chatadvisor.ParentGuidanceBlock,
"advisor guidance should not be injected when disabled")
}
"advisor tool must not be registered when the chat-advisor experiment is absent")
}
func TestAdvisorGating_RootChat(t *testing.T) {
+2
View File
@@ -118,6 +118,8 @@ func (server *Server) prepareGeneration(
planModeInstructions := server.loadPlanModeInstructions(ctx, currentPlanMode, logger)
advisorCfg := server.loadAdvisorConfig(ctx, logger)
// Force Enabled from the experiment; the stored DB value is ignored.
advisorCfg.Enabled = server.experiments.Enabled(codersdk.ExperimentChatAdvisor)
var advisorRuntime *chatadvisor.Runtime
if advisorCfg.Enabled && isRootChat && !isPlanModeTurn && !isExploreSubagent {
-8
View File
@@ -114,14 +114,6 @@ type listAgentsArgs struct {
Offset *int `json:"offset,omitempty"`
}
func (p *Server) isDesktopEnabled(ctx context.Context) bool {
enabled, err := p.db.GetChatDesktopEnabled(ctx)
if err != nil {
return false
}
return enabled
}
func subagentModelOverrideLogLabel(
overrideContext codersdk.ChatModelOverrideContext,
) string {
+2 -2
View File
@@ -114,8 +114,8 @@ func allSubagentDefinitions() []subagentDefinition {
if currentChat.PlanMode.Valid && currentChat.PlanMode.ChatPlanMode == database.ChatPlanModePlan {
return `type "computer_use" is unavailable in plan mode`
}
if !p.isDesktopEnabled(ctx) {
return `type "computer_use" is unavailable because desktop access is not enabled`
if !p.experiments.Enabled(codersdk.ExperimentChatVirtualDesktop) {
return `type "computer_use" is unavailable because the chat-virtual-desktop experiment is not enabled`
}
_, _, _, err := p.computerUseProviderAndModelFromConfig(ctx)
if err != nil {
@@ -96,7 +96,6 @@ func TestSpawnComputerUseAgentInheritsPinnedContext(t *testing.T) {
t.Parallel()
db, ps := dbtestutil.NewDB(t)
require.NoError(t, db.UpsertChatDesktopEnabled(chatdTestContext(t), true))
server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{})
ctx := chatdTestContext(t)
+22 -17
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"slices"
"sync"
"testing"
"time"
@@ -73,6 +74,7 @@ type internalTestServerConfig struct {
logger slog.Logger
clock quartz.Clock
startWorker bool
experiments codersdk.Experiments
}
type internalTestServerOpt func(*internalTestServerConfig)
@@ -95,6 +97,19 @@ func withInternalTestServerWorker() internalTestServerOpt {
}
}
func withInternalTestServerExperiments(experiments codersdk.Experiments) internalTestServerOpt {
return func(cfg *internalTestServerConfig) {
cfg.experiments = experiments
}
}
func experimentsOrDefault(experiments codersdk.Experiments) codersdk.Experiments {
if experiments == nil {
return codersdk.ExperimentsKnown
}
return experiments
}
// newInternalTestServer creates a passive Server for internal tests with
// custom provider API keys. Pass withInternalTestServerWorker to start the
// background chat worker for tests that need real execution.
@@ -123,6 +138,7 @@ func newInternalTestServer(
// does not interfere with test assertions.
PendingChatAcquireInterval: testutil.WaitLong,
ProviderAPIKeys: keys,
Experiments: experimentsOrDefault(cfg.experiments),
})
if cfg.startWorker {
server.Start()
@@ -2150,7 +2166,6 @@ func TestSpawnAgent_DescriptionListsAllAvailableTypes(t *testing.T) {
t.Parallel()
db, ps := dbtestutil.NewDB(t)
require.NoError(t, db.UpsertChatDesktopEnabled(chatdTestContext(t), true))
server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{
Anthropic: "test-anthropic-key",
})
@@ -2198,7 +2213,6 @@ func TestSpawnAgent_DescriptionIncludesComputerUseWithMissingProviderKey(t *test
t.Parallel()
db, ps := dbtestutil.NewDB(t)
require.NoError(t, db.UpsertChatDesktopEnabled(chatdTestContext(t), true))
server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{})
ctx := chatdTestContext(t)
@@ -2220,7 +2234,6 @@ func TestSpawnAgent_PlanModeDescriptionOmitsComputerUse(t *testing.T) {
t.Parallel()
db, ps := dbtestutil.NewDB(t)
require.NoError(t, db.UpsertChatDesktopEnabled(chatdTestContext(t), true))
server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{
Anthropic: "test-anthropic-key",
})
@@ -2261,7 +2274,6 @@ func TestSpawnAgent_PlanModeRejectsComputerUse(t *testing.T) {
t.Parallel()
db, ps := dbtestutil.NewDB(t)
require.NoError(t, db.UpsertChatDesktopEnabled(chatdTestContext(t), true))
server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{
Anthropic: "test-anthropic-key",
})
@@ -2312,7 +2324,6 @@ func TestSpawnAgent_InvalidTypeAndCredentialErrorAreDistinct(t *testing.T) {
t.Parallel()
db, ps := dbtestutil.NewDB(t)
require.NoError(t, db.UpsertChatDesktopEnabled(chatdTestContext(t), true))
server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{})
ctx := chatdTestContext(t)
@@ -2353,7 +2364,6 @@ func TestSpawnAgent_ComputerUseAvailabilityUsesConfiguredProvider(t *testing.T)
db, ps := dbtestutil.NewDB(t)
ctx := chatdTestContext(t)
require.NoError(t, db.UpsertChatDesktopEnabled(ctx, true))
require.NoError(t, db.UpsertChatComputerUseProvider(
ctx,
chattool.ComputerUseProviderOpenAI,
@@ -2374,7 +2384,6 @@ func TestSpawnAgent_ComputerUseRejectsMissingConfiguredProvider(t *testing.T) {
db, ps := dbtestutil.NewDB(t)
ctx := chatdTestContext(t)
require.NoError(t, db.UpsertChatDesktopEnabled(ctx, true))
require.NoError(t, db.UpsertChatComputerUseProvider(
ctx,
chattool.ComputerUseProviderOpenAI,
@@ -2434,7 +2443,6 @@ func TestSpawnAgent_ComputerUseRejectsInvalidConfiguredProviderWithStableReason(
db, ps := dbtestutil.NewDB(t)
ctx := chatdTestContext(t)
require.NoError(t, db.UpsertChatDesktopEnabled(ctx, true))
require.NoError(t, db.UpsertChatComputerUseProvider(ctx, "bogus"))
logSink := &subagentTestLogSink{}
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).AppendSinks(logSink)
@@ -2463,9 +2471,13 @@ func TestSpawnAgent_ComputerUseRejectsDesktopDisabled(t *testing.T) {
t.Parallel()
db, ps := dbtestutil.NewDB(t)
experiments := slices.DeleteFunc(
slices.Clone(codersdk.ExperimentsKnown),
func(e codersdk.Experiment) bool { return e == codersdk.ExperimentChatVirtualDesktop },
)
server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{
Anthropic: "test-anthropic-key",
})
}, withInternalTestServerExperiments(experiments))
ctx := chatdTestContext(t)
user, org, model := seedInternalChatDeps(t, db)
@@ -2478,14 +2490,13 @@ func TestSpawnAgent_ComputerUseRejectsDesktopDisabled(t *testing.T) {
Prompt: "open the browser",
})
require.True(t, resp.IsError)
require.Contains(t, resp.Content, `type "computer_use" is unavailable because desktop access is not enabled`)
require.Contains(t, resp.Content, `type "computer_use" is unavailable because the chat-virtual-desktop experiment is not enabled`)
}
func TestSpawnAgent_BlankTypeReturnsValidOptions(t *testing.T) {
t.Parallel()
db, ps := dbtestutil.NewDB(t)
require.NoError(t, db.UpsertChatDesktopEnabled(chatdTestContext(t), true))
server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{
Anthropic: "test-anthropic-key",
})
@@ -2527,7 +2538,6 @@ func TestSpawnAgent_NotAvailableForChildChats(t *testing.T) {
t.Parallel()
db, ps := dbtestutil.NewDB(t)
require.NoError(t, db.UpsertChatDesktopEnabled(chatdTestContext(t), true))
server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{
Anthropic: "test-anthropic-key",
})
@@ -2609,9 +2619,6 @@ func TestSubagentLifecycleToolsIncludePersistedSubagentTypeAcrossVariants(t *tes
t.Parallel()
db, ps := dbtestutil.NewDB(t)
if tt.variant == subagentTypeComputerUse {
require.NoError(t, db.UpsertChatDesktopEnabled(chatdTestContext(t), true))
}
server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{})
@@ -2750,7 +2757,6 @@ func TestSpawnAgent_ComputerUseUsesComputerUseModelNotParent(t *testing.T) {
t.Parallel()
db, ps := dbtestutil.NewDB(t)
require.NoError(t, db.UpsertChatDesktopEnabled(chatdTestContext(t), true))
server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{})
ctx := chatdTestContext(t)
@@ -2810,7 +2816,6 @@ func TestSpawnAgent_ComputerUseInheritsMCPServerIDs(t *testing.T) {
t.Parallel()
db, ps := dbtestutil.NewDB(t)
require.NoError(t, db.UpsertChatDesktopEnabled(chatdTestContext(t), true))
server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{})
ctx := chatdTestContext(t)