eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1package bot
2
3import (
4 "encoding/json"
5 "errors"
6 "net/http"
7 "net/http/httptest"
8 "sync"
9 "testing"
10)
11
12// The client retries transient failures (a dropped connection or a 5xx) and
13// succeeds once the API responds — so a single "unexpected EOF" on the long
14// build call no longer fails the whole build.
15func TestAnthropicRetriesTransient(t *testing.T) {
16 var mu sync.Mutex
17 n := 0
18 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19 mu.Lock()
20 n++
21 attempt := n
22 mu.Unlock()
23 switch attempt {
24 case 1:
25 // Drop the connection mid-response → the client sees a transport
26 // error (like unexpected EOF).
27 hj, ok := w.(http.Hijacker)
28 if !ok {
29 t.Fatal("no hijacker")
30 }
31 conn, _, _ := hj.Hijack()
32 conn.Close()
33 case 2:
34 w.WriteHeader(http.StatusServiceUnavailable)
35 w.Write([]byte("overloaded"))
36 default:
37 w.Write([]byte(`{"content":[{"type":"text","text":"ok"}],"stop_reason":"end_turn"}`))
38 }
39 }))
40 defer srv.Close()
41
42 c := &anthropicClient{key: "k", base: srv.URL, http: srv.Client()}
43 resp, err := c.create(anthRequest{Model: "m", MaxTokens: 10, Messages: []anthMessage{{Role: "user", Content: "hi"}}})
44 if err != nil {
45 t.Fatalf("create should have retried to success: %v", err)
46 }
47 if resp.text() != "ok" {
48 t.Fatalf("bad reply: %q", resp.text())
49 }
50 if n != 3 {
51 t.Fatalf("expected 3 attempts (drop, 503, ok), got %d", n)
52 }
53}
54
55// createStream assembles the response from SSE deltas — text and tool_use
56// accumulate across content blocks, and a request that ends with message_stop
57// succeeds.
58func TestAnthropicStreamAssembles(t *testing.T) {
59 stream := "event: message_start\n" +
60 `data: {"type":"message_start"}` + "\n\n" +
61 "event: content_block_start\n" +
62 `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}` + "\n\n" +
63 "event: content_block_delta\n" +
64 `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello "}}` + "\n\n" +
65 "event: content_block_delta\n" +
66 `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"world"}}` + "\n\n" +
67 "event: content_block_stop\n" +
68 `data: {"type":"content_block_stop","index":0}` + "\n\n" +
69 "event: content_block_start\n" +
70 `data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"t1","name":"do_it","input":{}}}` + "\n\n" +
71 "event: content_block_delta\n" +
72 `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"x\":"}}` + "\n\n" +
73 "event: content_block_delta\n" +
74 `data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"1}"}}` + "\n\n" +
75 "event: message_delta\n" +
76 `data: {"type":"message_delta","delta":{"stop_reason":"tool_use"}}` + "\n\n" +
77 "event: message_stop\n" +
78 `data: {"type":"message_stop"}` + "\n\n"
79
80 var gotStream bool
81 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
82 var body struct {
83 Stream bool `json:"stream"`
84 }
85 json.NewDecoder(r.Body).Decode(&body)
86 gotStream = body.Stream
87 w.Header().Set("Content-Type", "text/event-stream")
88 w.Write([]byte(stream))
89 }))
90 defer srv.Close()
91
92 c := &anthropicClient{key: "k", base: srv.URL, http: srv.Client()}
93 resp, err := c.createStream(anthRequest{Model: "m", MaxTokens: 10, Messages: []anthMessage{{Role: "user", Content: "hi"}}})
94 if err != nil {
95 t.Fatalf("createStream: %v", err)
96 }
97 if !gotStream {
98 t.Fatal("request did not set stream:true")
99 }
100 if resp.text() != "Hello world" {
101 t.Fatalf("text not assembled: %q", resp.text())
102 }
103 if resp.StopReason != "tool_use" {
104 t.Fatalf("stop reason: %q", resp.StopReason)
105 }
106 if len(resp.Content) != 2 || resp.Content[1].Type != "tool_use" || resp.Content[1].Name != "do_it" {
107 t.Fatalf("tool_use block missing: %+v", resp.Content)
108 }
109 if v, _ := resp.Content[1].Input["x"].(float64); v != 1 {
110 t.Fatalf("tool_use input not assembled: %+v", resp.Content[1].Input)
111 }
112}
113
114// A stream cut before message_stop is transient: the client retries and succeeds
115// on the next attempt — the streaming twin of the EOF-retry behaviour.
116func TestAnthropicStreamRetriesCutStream(t *testing.T) {
117 full := "event: content_block_start\n" +
118 `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}` + "\n\n" +
119 "event: content_block_delta\n" +
120 `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ok"}}` + "\n\n" +
121 "event: message_stop\n" +
122 `data: {"type":"message_stop"}` + "\n\n"
123 var mu sync.Mutex
124 n := 0
125 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
126 mu.Lock()
127 n++
128 attempt := n
129 mu.Unlock()
130 if attempt == 1 {
131 // Some deltas, then drop the connection with no message_stop.
132 hj, _ := w.(http.Hijacker)
133 conn, buf, _ := hj.Hijack()
134 buf.WriteString("HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n\r\n")
135 buf.WriteString("event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n")
136 buf.Flush()
137 conn.Close()
138 return
139 }
140 w.Header().Set("Content-Type", "text/event-stream")
141 w.Write([]byte(full))
142 }))
143 defer srv.Close()
144
145 c := &anthropicClient{key: "k", base: srv.URL, http: srv.Client()}
146 resp, err := c.createStream(anthRequest{Model: "m", MaxTokens: 10, Messages: []anthMessage{{Role: "user", Content: "hi"}}})
147 if err != nil {
148 t.Fatalf("createStream should have retried past the cut: %v", err)
149 }
150 if resp.text() != "ok" {
151 t.Fatalf("bad reply: %q", resp.text())
152 }
153 if n != 2 {
154 t.Fatalf("expected 2 attempts (cut, ok), got %d", n)
155 }
156}
157
158// A deterministic 4xx (bad request / auth) is NOT retried — it would just fail
159// the same way, so the caller learns immediately.
160func TestAnthropicDoesNotRetry4xx(t *testing.T) {
161 var mu sync.Mutex
162 n := 0
163 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
164 mu.Lock()
165 n++
166 mu.Unlock()
167 w.WriteHeader(http.StatusBadRequest)
168 w.Write([]byte(`{"error":{"message":"bad"}}`))
169 }))
170 defer srv.Close()
171
172 c := &anthropicClient{key: "k", base: srv.URL, http: srv.Client()}
173 _, err := c.create(anthRequest{Model: "m", MaxTokens: 10, Messages: []anthMessage{{Role: "user", Content: "hi"}}})
174 if err == nil {
175 t.Fatal("expected an error")
176 }
177 var he *anthHTTPError
178 if !errors.As(err, &he) || he.status != http.StatusBadRequest {
179 t.Fatalf("expected a 400 anthHTTPError, got %v", err)
180 }
181 mu.Lock()
182 defer mu.Unlock()
183 if n != 1 {
184 t.Fatalf("a 4xx must not be retried; got %d attempts", n)
185 }
186}
187