Files
coder/coderd/coderdtest/chat.go
T
Ethan 4e08543ace test(coderd): centralize chat test harness and stabilize flakes (#25171)
Chat tests previously constructed a real `openai` provider with a fake
API key and no `BaseURL`, so background title generation hit
`api.openai.com` and timed out under `-race`. The same root cause
produced several distinct flakes: title regeneration races with
synchronous `UpdateChat`/`ProposeChatTitle`, and pagination races
against `updated_at` bumps from real-network processing.

This moves the fake OpenAI-compatible provider and the chat-settle wait
into first-class `coderdtest` capabilities.
`coderd.Options.ChatProviderAPIKeys` is the new seam tests use to
redirect chat traffic to a local `httptest.Server`.
`coderdtest.WaitForChatSettled` replaces per-test waiters and drains
tracked chat-daemon work after the chat row leaves `pending`/`running`.
The `newChatClient*` constructors funnel through one options builder
that installs the fake provider before the coderd test server so cleanup
ordering is deterministic.

Closes https://github.com/coder/internal/issues/1528 & Closes ENG-2659
Closes https://github.com/coder/internal/issues/1480 & Closes CODAGT-359
Closes https://github.com/coder/internal/issues/1507 & Closes CODAGT-368
Relates to https://github.com/coder/internal/issues/1397 & Relates to
CODAGT-374
2026-05-12 22:13:55 +10:00

129 lines
3.8 KiB
Go

package coderdtest
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/x/chatd"
"github.com/coder/coder/v2/coderd/x/chatd/chatprovider"
"github.com/coder/coder/v2/coderd/x/chatd/chattest"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"
)
const (
// TestChatProviderOpenAICompat is the default provider for chat runtime tests.
TestChatProviderOpenAICompat = "openai-compat"
// TestChatProviderAPIKey is a non-secret API key for local chat providers.
TestChatProviderAPIKey = "test-api-key"
// TestChatModelOpenAICompat is the default model for chat runtime tests.
TestChatModelOpenAICompat = "gpt-4o-mini"
)
// OpenAICompatProviderAPIKeys returns provider keys that route OpenAI-compatible
// chat calls to baseURL.
func OpenAICompatProviderAPIKeys(baseURL string) chatprovider.ProviderAPIKeys {
return chatprovider.ProviderAPIKeys{
ByProvider: map[string]string{
TestChatProviderOpenAICompat: TestChatProviderAPIKey,
},
BaseURLByProvider: map[string]string{
TestChatProviderOpenAICompat: baseURL,
},
}
}
// FakeOpenAICompatProviderAPIKeys starts a fake OpenAI-compatible provider and
// returns provider keys for coderdtest.Options.
func FakeOpenAICompatProviderAPIKeys(t testing.TB) chatprovider.ProviderAPIKeys {
t.Helper()
return OpenAICompatProviderAPIKeys(chattest.OpenAI(t))
}
// CreateOpenAICompatChatModelConfig creates the default provider and model
// config used by chat runtime tests. Tests that create chats should also set
// Options.ChatProviderAPIKeys, usually via FakeOpenAICompatProviderAPIKeys, so
// background chat work routes to a local provider until coderd closes. baseURL,
// when non-empty, is stored on the provider config.
func CreateOpenAICompatChatModelConfig(
t testing.TB,
client *codersdk.ExperimentalClient,
baseURL string,
) codersdk.ChatModelConfig {
t.Helper()
ctx := testutil.Context(t, testutil.WaitLong)
_, err := client.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: TestChatProviderOpenAICompat,
APIKey: TestChatProviderAPIKey,
BaseURL: baseURL,
})
require.NoError(t, err)
contextLimit := int64(4096)
isDefault := true
modelConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
Provider: TestChatProviderOpenAICompat,
Model: TestChatModelOpenAICompat,
ContextLimit: &contextLimit,
IsDefault: &isDefault,
})
require.NoError(t, err)
return modelConfig
}
// WaitForChatSettled waits for a chat to leave active processing and drains
// tracked chat daemon work before returning the final row.
func WaitForChatSettled(
ctx context.Context,
t testing.TB,
api *coderd.API,
chatID uuid.UUID,
) database.Chat {
t.Helper()
require.NotNil(t, api)
waitForChatTerminalState(ctx, t, api.Database, chatID)
server := api.ChatDaemonForTest()
require.NotNil(t, server)
chatd.WaitUntilIdleForTest(server)
chat, err := getChatByIDAsSystem(ctx, api.Database, chatID)
require.NoError(t, err)
return chat
}
func waitForChatTerminalState(
ctx context.Context,
t testing.TB,
db database.Store,
chatID uuid.UUID,
) {
t.Helper()
require.Eventually(t, func() bool {
chat, err := getChatByIDAsSystem(ctx, db, chatID)
if err != nil {
return false
}
return chat.Status != database.ChatStatusPending && chat.Status != database.ChatStatusRunning
}, testutil.WaitLong, testutil.IntervalFast)
}
func getChatByIDAsSystem(
ctx context.Context,
db database.Store,
chatID uuid.UUID,
) (database.Chat, error) {
// Test helper needs system scope to observe chatd-owned status changes.
//nolint:gocritic
return db.GetChatByID(dbauthz.AsSystemRestricted(ctx), chatID)
}