mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user