mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
> **PR Stack** > 1. #23351 ← `#23282` > 2. #23282 ← `#23275` > 3. **#23275** ← `#23349` *(you are here)* > 4. #23349 ← `main` --- ## Summary Extracts a structured error classification subsystem for agent chat (`chatd`) so that retry and error payloads carry machine-readable metadata — error kind, provider name, HTTP status code, and retryability — instead of raw error strings. This is the **backend half** of the error-handling work. The frontend counterpart is in #23282. ## Changes ### New package: `coderd/chatd/chaterror/` Canonical error classification — extracts error kind, provider, status code, and user-facing message from raw provider errors. One source of truth that drives both retry policy and stream payloads. - **`kind.go`**: Error kind enum (`rate_limit`, `timeout`, `auth`, `config`, `overloaded`, `unknown`). - **`signals.go`**: Signal extraction — parses provider name, HTTP status code, and retryability from error strings and wrapped types. - **`classify.go`**: Classification logic — maps extracted signals to an error kind. - **`message.go`**: User-facing message templates keyed by kind + signals. - **`payload.go`**: Projectors that build `ChatStreamError` and `ChatStreamRetry` payloads from a classified error. ### Modified - **`codersdk/chats.go`**: Added `Kind`, `Provider`, `Retryable`, `StatusCode` fields to `ChatStreamError` and `ChatStreamRetry`. - **`coderd/chatd/chatretry/`**: Thinned to retry-policy only; classification logic moved to `chaterror`. - **`coderd/chatd/chatloop/`**: Added per-attempt first-chunk timeout (60 s) via `guardedStream` wrapper — produces retryable `startup_timeout` errors instead of hanging forever. - **`coderd/chatd/chatd.go`**: Publishes normalized retry/error payloads via `chaterror` projectors.
70 lines
2.5 KiB
Go
70 lines
2.5 KiB
Go
package chaterror_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/x/chatd/chaterror"
|
|
)
|
|
|
|
func TestExtractStatusCode(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want int
|
|
}{
|
|
{name: "Status", input: "received status 429 from upstream", want: 429},
|
|
{name: "StatusCode", input: "status code: 503", want: 503},
|
|
{name: "HTTP", input: "http 502 bad gateway", want: 502},
|
|
{name: "Standalone", input: "got 504 from upstream", want: 504},
|
|
{name: "MultipleStandaloneCodesReturnFirstMatch", input: "retrying 503 after 429", want: 503},
|
|
{name: "MixedCaseViaCallerLowering", input: "HTTP 503 bad gateway", want: 503},
|
|
{name: "PortNumberIPIsNotStatus", input: "dial tcp 10.0.0.1:503: connection refused", want: 0},
|
|
{name: "PortNumberHostIsNotStatus", input: "proxy.internal:502 unreachable", want: 0},
|
|
{name: "PortNumberDialIsNotStatus", input: "dial tcp 172.16.0.5:429: refused", want: 0},
|
|
{name: "PortThenRealStatusReturnsRealStatus", input: "proxy at 10.0.0.1:500 returned 503", want: 503},
|
|
{name: "NoFabricatedOverloadStatus", input: "anthropic overloaded_error", want: 0},
|
|
{name: "NoFabricatedRateLimitStatus", input: "too many requests", want: 0},
|
|
{name: "NoFabricatedBadGatewayStatus", input: "bad gateway", want: 0},
|
|
{name: "NoFabricatedServiceUnavailableStatus", input: "service unavailable", want: 0},
|
|
{name: "NoStatus", input: "boom", want: 0},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
require.Equal(t, tt.want, chaterror.ExtractStatusCodeForTest(strings.ToLower(tt.input)))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDetectProvider(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
}{
|
|
{name: "OpenAICompatBeatsOpenAI", input: "openai-compat upstream error", want: "openai-compat"},
|
|
{name: "OpenAICompatibleAlias", input: "openai compatible proxy", want: "openai-compat"},
|
|
{name: "AzureOpenAI", input: "azure openai rate limited", want: "azure"},
|
|
{name: "OpenAI", input: "openai rate limited", want: "openai"},
|
|
{name: "Anthropic", input: "anthropic overloaded", want: "anthropic"},
|
|
{name: "GoogleGemini", input: "gemini timeout", want: "google"},
|
|
{name: "Vercel", input: "vercel ai gateway 503", want: "vercel"},
|
|
{name: "Unknown", input: "local provider error", want: ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
require.Equal(t, tt.want, chaterror.DetectProviderForTest(strings.ToLower(tt.input)))
|
|
})
|
|
}
|
|
}
|