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}