error_test.go

 1package anthropic
 2
 3import (
 4	"errors"
 5	"fmt"
 6	"io"
 7	"testing"
 8
 9	"charm.land/fantasy"
10)
11
12func TestToProviderErr_WrapsUnexpectedEOF(t *testing.T) {
13	t.Parallel()
14
15	cases := []struct {
16		name string
17		err  error
18	}{
19		{"direct", io.ErrUnexpectedEOF},
20		{"wrapped", fmt.Errorf("read stream: %w", io.ErrUnexpectedEOF)},
21		{"double_wrapped", fmt.Errorf("anthropic: %w", fmt.Errorf("sse: %w", io.ErrUnexpectedEOF))},
22	}
23
24	for _, tc := range cases {
25		tc := tc
26		t.Run(tc.name, func(t *testing.T) {
27			t.Parallel()
28
29			got := toProviderErr(tc.err)
30
31			var providerErr *fantasy.ProviderError
32			if !errors.As(got, &providerErr) {
33				t.Fatalf("toProviderErr did not wrap %v as *fantasy.ProviderError (got %T)", tc.err, got)
34			}
35			if !errors.Is(providerErr.Cause, io.ErrUnexpectedEOF) {
36				t.Errorf("ProviderError.Cause = %v, want chain containing io.ErrUnexpectedEOF", providerErr.Cause)
37			}
38			if !providerErr.IsRetryable() {
39				t.Error("wrapped io.ErrUnexpectedEOF must be retryable so retry.go engages")
40			}
41		})
42	}
43}
44
45func TestToProviderErr_PassesThroughUnrelatedErrors(t *testing.T) {
46	t.Parallel()
47
48	err := errors.New("something unrelated")
49	got := toProviderErr(err)
50	if got != err {
51		t.Errorf("toProviderErr mutated unrelated error: got %v, want %v", got, err)
52	}
53}
54
55func TestToProviderErr_PassesThroughPlainEOF(t *testing.T) {
56	t.Parallel()
57
58	// A clean io.EOF at the end of a stream is not a failure — the streaming
59	// handler in anthropic.go treats it as a normal terminator and never
60	// calls toProviderErr with io.EOF. But if it ever did, we should not
61	// wrap it: io.EOF is not "retryable" in the ProviderError sense.
62	got := toProviderErr(io.EOF)
63	var providerErr *fantasy.ProviderError
64	if errors.As(got, &providerErr) {
65		t.Errorf("toProviderErr wrapped io.EOF as ProviderError; should pass through")
66	}
67}