Files
coder/enterprise/aibridgeproxyd/aibridgeproxyd_internal_test.go
Danny Kopping a85462bd49 feat: support adding GitHub Copilot AI provider via UI (#25888)
Copilot is the only AI provider type that could not be added through the `/ai/settings` UI. The aibridge runtime and the env-var seeding path already supported it, but the runtime CRUD API rejected `type=copilot` and the UI omitted it entirely. The root cause is that Copilot's auth model (a per-request GitHub OAuth token, with no pre-shared key) does not fit the credential-centric add-provider flow that every other provider uses.

## Backend

Allow `type=copilot` in `CreateAIProviderRequest.Validate()`, and reject `api_keys` for Copilot on both create (validation) and update (handler sentinel), mirroring the existing Bedrock guards. Copilot carries no stored credential.

## Frontend

Add Copilot to the provider type picker (with the `github-copilot.svg` icon) and give the form a credential-free branch: name, display name, and a free-text endpoint defaulting to `https://api.business.githubcopilot.com`, with copy explaining that authentication happens via the user's GitHub token at request time. Copilot maps to the distinct `copilot` wire type rather than collapsing to `openai`, and the edit flow recovers it correctly.

The endpoint stays required with a business-tier default; users on the individual or enterprise endpoints edit the field.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-01 15:26:37 +02:00

69 lines
2.0 KiB
Go

package aibridgeproxyd
import (
"bytes"
"io"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/require"
"cdr.dev/slog/v3/sloggers/slogtest"
)
// TestReadErrorBodyForLog verifies that reading an aibridged error
// response body for logging leaves the body intact for downstream
// consumers (the proxy forwards it, and the response dumper reads it
// again), and that the logged rendering is capped.
func TestReadErrorBodyForLog(t *testing.T) {
t.Parallel()
newResponse := func(body string) *http.Response {
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(strings.NewReader(body)),
}
}
t.Run("ReturnsBodyAndRestores", func(t *testing.T) {
t.Parallel()
s := &Server{ctx: t.Context(), logger: slogtest.Make(t, nil)}
resp := newResponse(`{"error":"bad request"}`)
got := s.readErrorBodyForLog(resp, s.logger)
require.Equal(t, `{"error":"bad request"}`, got)
// The body must still be readable in full for the proxy and the
// response dumper.
restored, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, `{"error":"bad request"}`, string(restored))
})
t.Run("TruncatesLargeBodyButRestoresFull", func(t *testing.T) {
t.Parallel()
s := &Server{ctx: t.Context(), logger: slogtest.Make(t, nil)}
full := bytes.Repeat([]byte("a"), maxLoggedErrorBodyBytes+512)
resp := newResponse(string(full))
got := s.readErrorBodyForLog(resp, s.logger)
require.Len(t, got, maxLoggedErrorBodyBytes+len("...(truncated)"))
require.True(t, strings.HasSuffix(got, "...(truncated)"))
// Truncation only affects the log string; the restored body is
// the complete payload.
restored, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, full, restored)
})
t.Run("NilBody", func(t *testing.T) {
t.Parallel()
s := &Server{ctx: t.Context(), logger: slogtest.Make(t, nil)}
resp := &http.Response{StatusCode: http.StatusInternalServerError, Body: nil}
require.Equal(t, "", s.readErrorBodyForLog(resp, s.logger))
})
}