mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
## Stack context This is the second PR in the 3-PR chat API promotion stack: server compatibility mounts (#28496), codersdk promotion (this PR), and frontend path updates (#28498). ## Summary Move the promoted chat and MCP SDK methods from `ExperimentalClient` to `Client` and update them to use `/api/v2`. Methods for routes that remain experimental stay on `ExperimentalClient`. Update in-repository callers and generated types. The multi-replica chat stream relay dials `/api/v2` directly: mixed-version replica sets are not a supported upgrade path, so no experimental-path fallback is kept (per review). Remote dogfood UAT passed for the composed stack. > [!NOTE] > Xum acted on Mike's behalf in this pull request. <!-- xum-attribution: model=claude-fable-5 thinking=high -->
142 lines
4.2 KiB
Go
142 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)
|
|
|
|
defaultOrg, err := client.OrganizationByName(ctx, codersdk.DefaultOrganization)
|
|
require.NoError(t, err)
|
|
configs, err := client.ChatModels(ctx, defaultOrg.ID)
|
|
require.NoError(t, err)
|
|
matchingConfigs := scaletestModelConfigsForProvider(configs.Models, provider.ID)
|
|
require.Len(t, matchingConfigs, 1)
|
|
require.True(t, matchingConfigs[0].Enabled)
|
|
|
|
chats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{Query: "archived:true"})
|
|
require.NoError(t, err)
|
|
|
|
var scaletestMessages []codersdk.ChatMessage
|
|
for _, chat := range chats {
|
|
resp, err := client.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.ChatModel, providerID uuid.UUID) []codersdk.ChatModel {
|
|
matches := make([]codersdk.ChatModel, 0, 1)
|
|
for _, config := range configs {
|
|
if config.AIProviderID != providerID {
|
|
continue
|
|
}
|
|
if config.Model != "scaletest-model" {
|
|
continue
|
|
}
|
|
matches = append(matches, config)
|
|
}
|
|
return matches
|
|
}
|