mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: gate chat advisor and virtual desktop behind experiments, delete experiments page (#26809)
This commit is contained in:
Generated
+11
-3
@@ -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": {
|
||||
|
||||
Generated
+11
-3
@@ -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
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user