Files
coder/coderd/x/chatd/chaterror/payload_test.go
T
Ethan b6dbc5614c fix(coderd/x/chatd): handle truncated provider streams (#25074)
coder/fantasy now fails closed when Anthropic or OpenAI Responses
streams close before their provider terminal events instead of yielding
a successful finish.

This bumps the fantasy replacement to coder/fantasy#33 and teaches chat
error classification to treat those failures as retryable timeout errors
with explicit stream-closed messages.

<img width="875" height="311" alt="image"
src="https://github.com/user-attachments/assets/69c6f7b5-c885-46d2-a88b-b7a2b111bd55"
/>
2026-05-08 15:52:42 +10:00

94 lines
2.6 KiB
Go

package chaterror_test
import (
"io"
"testing"
"time"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/x/chatd/chaterror"
"github.com/coder/coder/v2/codersdk"
)
func TestTerminalErrorPayloadUsesNormalizedClassification(t *testing.T) {
t.Parallel()
classified := chaterror.Classify(
xerrors.New("azure openai received status 429 from upstream"),
)
payload := chaterror.TerminalErrorPayload(classified)
require.Equal(t, &codersdk.ChatError{
Message: "Azure OpenAI is rate limiting requests.",
Kind: codersdk.ChatErrorKindRateLimit,
Provider: "azure",
Retryable: true,
StatusCode: 429,
}, payload)
}
func TestTerminalErrorPayloadIncludesProviderDetail(t *testing.T) {
t.Parallel()
payload := chaterror.TerminalErrorPayload(chaterror.Classify(testProviderError(
"",
400,
nil,
testProviderResponseDump(`{"error":{"message":"Image exceeds 5 MB maximum."}}`),
)))
require.Equal(t, "Image exceeds 5 MB maximum.", payload.Detail)
}
func TestTerminalErrorPayloadNilForEmptyClassification(t *testing.T) {
t.Parallel()
require.Nil(t, chaterror.TerminalErrorPayload(chaterror.ClassifiedError{}))
}
func TestStreamRetryPayloadPreservesRetryableMessage(t *testing.T) {
t.Parallel()
delay := 3 * time.Second
classified := chaterror.Classify(xerrors.Errorf(
"anthropic stream closed before message_stop: %w",
io.EOF,
))
payload := chaterror.StreamRetryPayload(2, delay, classified)
require.NotNil(t, payload)
require.Equal(t,
"Anthropic stream closed unexpectedly before the response completed.",
payload.Error,
)
require.Equal(t, codersdk.ChatErrorKindTimeout, payload.Kind)
require.Equal(t, "anthropic", payload.Provider)
}
func TestStreamRetryPayloadUsesNormalizedClassification(t *testing.T) {
t.Parallel()
delay := 3 * time.Second
startedAt := time.Now()
payload := chaterror.StreamRetryPayload(2, delay, chaterror.ClassifiedError{
Message: "OpenAI returned an unexpected error.",
Kind: codersdk.ChatErrorKindGeneric,
Provider: "openai",
Retryable: true,
StatusCode: 503,
})
require.NotNil(t, payload)
require.Equal(t, 2, payload.Attempt)
require.Equal(t, delay.Milliseconds(), payload.DelayMs)
// Retry messages omit the HTTP status code; the status code is
// surfaced separately in the payload's StatusCode field.
require.Equal(t, "OpenAI returned an unexpected error.", payload.Error)
require.Equal(t, codersdk.ChatErrorKindGeneric, payload.Kind)
require.Equal(t, "openai", payload.Provider)
require.Equal(t, 503, payload.StatusCode)
require.WithinDuration(t, startedAt.Add(delay), payload.RetryingAt, time.Second)
}