Files
coder/cli/exp_scaletest_chat_test.go
T
Mathias Fredriksson 047c47495b refactor: drop chat_model_configs provider column (#26877)
The provider type already lives authoritatively in ai_providers.type,
reachable on every active row through ai_provider_id, which the
chat_model_configs_ai_provider_required_when_active CHECK makes
mandatory. The stored provider string was a denormalized copy the system
kept in sync with a startup backfill and no longer needs.

Every surface now derives provider type from the linked ai_providers
row. Telemetry is the one exception: it keeps emitting provider, now
sourced from ai_providers.type via a JOIN, so the BigQuery column and the
Nexus dashboards that read it are unaffected. The experimental HTTP/SDK
response drops provider and makes ai_provider_id required, since those
endpoints return only active configs; consumers resolve provider type
from ai_provider_id and the AI providers listing.

This ships in a single release with no compatibility window: production
reads the table via SELECT *, so a pre-drop binary fails config reads the
moment the column is gone. Operators must scale to zero before upgrading,
and there is no rollback.

Closes CODAGT-599
2026-07-01 15:59:55 +03:00

141 lines
4.2 KiB
Go

//go:build !slim
package cli_test
import (
"bytes"
"context"
"io"
"strings"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"cdr.dev/slog/v3"
"cdr.dev/slog/v3/sloggers/sloghuman"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/aibridgedtest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/scaletest/llmmock"
"github.com/coder/coder/v2/testutil"
)
const scaletestChatPrompt = "Reply with one short sentence from the scaletest."
func TestScaleTestChat(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
values := coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) {
require.NoError(t, dv.AI.BridgeConfig.Enabled.Set("true"))
})
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
DeploymentValues: values,
})
aibridgedtest.StartTestAIBridgeDaemon(t.Context(), t, api, nil)
coderdtest.CreateFirstUser(t, client)
server := new(llmmock.Server)
require.NoError(t, server.Start(context.Background(), llmmock.Config{
Address: "127.0.0.1:0",
Logger: slog.Make(sloghuman.Sink(io.Discard)).Leveled(slog.LevelDebug),
}))
t.Cleanup(func() {
require.NoError(t, server.Stop())
})
mockURL := server.APIAddress() + "/v1"
inv, root := clitest.New(t,
"exp", "scaletest", "chat",
"--chats-per-workspace", "1",
"--turns", "1",
"--prompt", scaletestChatPrompt,
"--timeout", "30s",
"--job-timeout", "30s",
"--cleanup-timeout", "30s",
"--cleanup-job-timeout", "30s",
"--scaletest-prometheus-address", "127.0.0.1:0",
"--scaletest-prometheus-wait", "0s",
"--provider-propagation-wait", "10ms",
"--llm-mock-url", mockURL,
)
//nolint:gocritic // The scaletest chat command requires an admin client.
clitest.SetupConfig(t, client, root)
var stderr bytes.Buffer
inv.Stdout = io.Discard
inv.Stderr = &stderr
err := inv.WithContext(ctx).Run()
require.NoError(t, err, stderr.String())
require.Contains(t, stderr.String(), "Scale test passed: 1/1 runs succeeded")
provider, err := client.AIProvider(ctx, "coder-scaletest-mock")
require.NoError(t, err)
require.Equal(t, mockURL, provider.BaseURL)
expClient := codersdk.NewExperimentalClient(client)
configs, err := expClient.ListChatModelConfigs(ctx)
require.NoError(t, err)
matchingConfigs := scaletestModelConfigsForProvider(configs, provider.ID)
require.Len(t, matchingConfigs, 1)
require.True(t, matchingConfigs[0].Enabled)
chats, err := expClient.ListChats(ctx, &codersdk.ListChatsOptions{Query: "archived:true"})
require.NoError(t, err)
var scaletestMessages []codersdk.ChatMessage
for _, chat := range chats {
resp, err := expClient.GetChatMessages(ctx, chat.ID, nil)
require.NoError(t, err)
if userText, ok := chatMessageText(resp.Messages, codersdk.ChatMessageRoleUser); ok &&
strings.Contains(userText, scaletestChatPrompt) {
scaletestMessages = resp.Messages
break
}
}
require.NotEmpty(t, scaletestMessages)
assistantText, ok := chatMessageText(scaletestMessages, codersdk.ChatMessageRoleAssistant)
require.True(t, ok, "expected an assistant reply in the scaletest chat")
require.NotEmpty(t, assistantText)
}
// chatMessageText concatenates the text parts of every message with the given
// role, reporting whether any such message was found. It aggregates across
// messages because the API returns them newest-first and a turn can produce
// more than one message per role.
func chatMessageText(messages []codersdk.ChatMessage, role codersdk.ChatMessageRole) (string, bool) {
var (
b strings.Builder
found bool
)
for _, msg := range messages {
if msg.Role != role {
continue
}
found = true
for _, part := range msg.Content {
if part.Type == codersdk.ChatMessagePartTypeText {
_, _ = b.WriteString(part.Text)
}
}
}
return b.String(), found
}
func scaletestModelConfigsForProvider(configs []codersdk.ChatModelConfig, providerID uuid.UUID) []codersdk.ChatModelConfig {
matches := make([]codersdk.ChatModelConfig, 0, 1)
for _, config := range configs {
if config.AIProviderID != providerID {
continue
}
if config.Model != "scaletest-model" {
continue
}
matches = append(matches, config)
}
return matches
}