| 1 | package provider |
| 2 | |
| 3 | import ( |
| 4 | "bufio" |
| 5 | "bytes" |
| 6 | "context" |
| 7 | "crypto/x509" |
| 8 | "encoding/json" |
| 9 | "errors" |
| 10 | "fmt" |
| 11 | "io" |
| 12 | "log" |
| 13 | "net" |
| 14 | "net/http" |
| 15 | "net/http/httptest" |
| 16 | "os" |
| 17 | "reflect" |
| 18 | "regexp" |
| 19 | "strings" |
| 20 | "sync/atomic" |
| 21 | "testing" |
| 22 | "time" |
| 23 | ) |
| 24 | |
| 25 | // canary is planted in every response a test scripts. No error this |
| 26 | // package returns may contain it: a provider's response body is content |
| 27 | // (gate 31), and a JSON decode error quotes the byte it failed on. |
| 28 | const canary = "CANARY-7f3a-passage-content" |
| 29 | |
| 30 | // stub serves one scripted reply and records what it was sent. |
| 31 | type stub struct { |
| 32 | srv *httptest.Server |
| 33 | status int |
| 34 | body string |
| 35 | delay time.Duration |
| 36 | calls atomic.Int32 |
| 37 | gotReq atomic.Value // chatRequest |
| 38 | gotKey atomic.Value // string |
| 39 | gotMethod atomic.Value // string |
| 40 | gotPath atomic.Value // string |
| 41 | // done is closed when the server sees the request's context end — |
| 42 | // which is how a test observes cancellation AT THE LISTENER, rather |
| 43 | // than trusting the error the client returned. |
| 44 | done chan struct{} |
| 45 | } |
| 46 | |
| 47 | func newStub(t *testing.T, status int, body string) *stub { |
| 48 | t.Helper() |
| 49 | s := &stub{status: status, body: body, done: make(chan struct{})} |
| 50 | s.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 51 | s.calls.Add(1) |
| 52 | s.gotMethod.Store(r.Method) |
| 53 | s.gotPath.Store(r.URL.Path) |
| 54 | s.gotKey.Store(r.Header.Get("Authorization")) |
| 55 | var req chatRequest |
| 56 | raw, _ := io.ReadAll(r.Body) |
| 57 | _ = json.Unmarshal(raw, &req) |
| 58 | s.gotReq.Store(req) |
| 59 | if s.delay > 0 { |
| 60 | select { |
| 61 | case <-time.After(s.delay): |
| 62 | case <-r.Context().Done(): |
| 63 | close(s.done) |
| 64 | return |
| 65 | } |
| 66 | } |
| 67 | w.WriteHeader(s.status) |
| 68 | _, _ = io.WriteString(w, s.body) |
| 69 | })) |
| 70 | t.Cleanup(s.srv.Close) |
| 71 | return s |
| 72 | } |
| 73 | |
| 74 | func (s *stub) client() *Client { |
| 75 | c, err := newClient("test-key", s.srv.URL) |
| 76 | if err != nil { |
| 77 | panic(err) |
| 78 | } |
| 79 | return c |
| 80 | } |
| 81 | |
| 82 | // reply wraps model content in an OpenAI-compatible envelope. |
| 83 | func reply(content, finish string) string { |
| 84 | b, _ := json.Marshal(map[string]any{ |
| 85 | "choices": []map[string]any{{ |
| 86 | "message": map[string]string{"role": "assistant", "content": content}, |
| 87 | "finish_reason": finish, |
| 88 | }}, |
| 89 | }) |
| 90 | return string(b) |
| 91 | } |
| 92 | |
| 93 | // allowedErrors is the complete set of error strings this package may |
| 94 | // return, built from its own sentinels so it cannot drift from them. A |
| 95 | // status error is the one parameterised case: the sentinel followed by |
| 96 | // exactly a three-digit code. |
| 97 | // |
| 98 | // **An exact set, not a canary search.** The first version checked only |
| 99 | // that the full canary was absent, and a review returned the first eight |
| 100 | // bytes of the response in the status error — "CANARY-7" — and stayed |
| 101 | // green. A pattern loose enough to admit "non-2xx" would admit lowercase |
| 102 | // content too; an exact set admits nothing it does not name. |
| 103 | var allowedErrors = map[string]bool{ |
| 104 | ErrTooLarge.Error(): true, ErrMalformed.Error(): true, ErrTruncated.Error(): true, |
| 105 | ErrForbiddenHost.Error(): true, ErrTransport.Error(): true, ErrMissingKey.Error(): true, |
| 106 | context.Canceled.Error(): true, context.DeadlineExceeded.Error(): true, |
| 107 | } |
| 108 | |
| 109 | var statusError = regexp.MustCompile(`^` + regexp.QuoteMeta(ErrStatus.Error()) + ` [0-9]{3}$`) |
| 110 | |
| 111 | func assertNoCanary(t *testing.T, err error) { |
| 112 | t.Helper() |
| 113 | if err != nil && !allowedErrors[err.Error()] && !statusError.MatchString(err.Error()) { |
| 114 | t.Errorf("error is not one of the fixed strings, so it may carry content: %q", err.Error()) |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | // The presence half every negative test below depends on: a well-formed |
| 119 | // reply is decoded into pointers, and the request is the one §8.9 says. |
| 120 | func TestExtractAddressesHappyPath(t *testing.T) { |
| 121 | s := newStub(t, 200, reply(`{"addresses":[{"passage":1,"text":"12 Main Street, Dublin 2"}]}`, "stop")) |
| 122 | got, err := s.client().ExtractAddresses(context.Background(), []string{"first", "At 12 Main Street, Dublin 2 we"}) |
| 123 | if err != nil { |
| 124 | t.Fatalf("extract: %v", err) |
| 125 | } |
| 126 | if len(got) != 1 || got[0].Passage != 1 || got[0].Text != "12 Main Street, Dublin 2" { |
| 127 | t.Fatalf("pointers = %+v", got) |
| 128 | } |
| 129 | |
| 130 | if m := s.gotMethod.Load(); m != http.MethodPost { |
| 131 | t.Errorf("method = %v, want POST", m) |
| 132 | } |
| 133 | if p := s.gotPath.Load(); p != "/chat/completions" { |
| 134 | t.Errorf("path = %v, want /chat/completions", p) |
| 135 | } |
| 136 | req := s.gotReq.Load().(chatRequest) |
| 137 | if len(req.Messages) != 2 || req.Messages[0].Role != "system" || req.Messages[0].Content != instructions || instructions == "" { |
| 138 | t.Errorf("the system message is not the extraction instructions") |
| 139 | } |
| 140 | if req.Model != Model { |
| 141 | t.Errorf("model = %q, want the pinned %q", req.Model, Model) |
| 142 | } |
| 143 | if req.ResponseFormat["type"] != "json_object" || req.Temperature != 0 { |
| 144 | t.Errorf("response_format/temperature = %v/%v", req.ResponseFormat, req.Temperature) |
| 145 | } |
| 146 | if s.gotKey.Load().(string) != "Bearer test-key" { |
| 147 | t.Errorf("authorization header wrong") |
| 148 | } |
| 149 | // The passages go as indexed data, in the order given — the caller |
| 150 | // resolves Pointer.Passage against that order. |
| 151 | // Every index and every text, as given — a review blanked passage 0 |
| 152 | // before serialising and a check of passage 1 alone stayed green. |
| 153 | var sent []sentPassage |
| 154 | want := []sentPassage{{0, "first"}, {1, "At 12 Main Street, Dublin 2 we"}} |
| 155 | if err := json.Unmarshal([]byte(req.Messages[1].Content), &sent); err != nil || !reflect.DeepEqual(sent, want) { |
| 156 | t.Errorf("passages sent = %+v, want %+v (err %v)", sent, want, err) |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | // "None found" is an empty list, and it is an answer, not an error. |
| 161 | func TestAnEmptyListIsAnAnswer(t *testing.T) { |
| 162 | s := newStub(t, 200, reply(`{"addresses":[]}`, "stop")) |
| 163 | got, err := s.client().ExtractAddresses(context.Background(), []string{"x"}) |
| 164 | if err != nil || len(got) != 0 { |
| 165 | t.Fatalf("got %+v, %v; want an empty answer", got, err) |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | func TestEveryFailureIsAnErrorAndCarriesNoContent(t *testing.T) { |
| 170 | big := `{"addresses":[{"passage":0,"text":"` + strings.Repeat("x", maxResponse) + `"}]}` |
| 171 | for _, c := range []struct { |
| 172 | name string |
| 173 | status int |
| 174 | body string |
| 175 | want error |
| 176 | }{ |
| 177 | // An OpenAI-compatible 4xx echoes the input that caused it. |
| 178 | {"a 400 echoing the input", 400, `{"error":{"message":"bad input: ` + canary + `"}}`, ErrStatus}, |
| 179 | {"a 500", 500, canary, ErrStatus}, |
| 180 | // A capped body that parsed as a shorter list is the omission |
| 181 | // §8.9 cannot otherwise see; the cap has to be detectable. |
| 182 | {"a body past the cap", 200, reply(big, "stop"), ErrTooLarge}, |
| 183 | // §8.9: a truncated response is rejected WHOLE. |
| 184 | {"truncated by the provider", 200, reply(`{"addresses":[{"passage":0,"text":"`+canary+`"}]}`, "length"), ErrTruncated}, |
| 185 | {"a content filter stop", 200, reply(`{"addresses":[]}`, "content_filter"), ErrMalformed}, |
| 186 | {"not json at all", 200, reply(canary, "stop"), ErrMalformed}, |
| 187 | // A json.SyntaxError quotes the byte it choked on. |
| 188 | {"broken json quoting content", 200, reply(`{"addresses":[{"passage":0,"text":"`+canary+`}`, "stop"), ErrMalformed}, |
| 189 | // The steerable parse: a duplicate key smuggles a second list. |
| 190 | {"a duplicate key", 200, reply(`{"addresses":[],"ADDRESSES":[{"passage":0,"text":"`+canary+`"}]}`, "stop"), ErrMalformed}, |
| 191 | {"an unknown field", 200, reply(`{"addresses":[],"note":"`+canary+`"}`, "stop"), ErrMalformed}, |
| 192 | {"trailing content", 200, reply(`{"addresses":[]}{"addresses":[{"passage":0,"text":"`+canary+`"}]}`, "stop"), ErrMalformed}, |
| 193 | {"null is not empty", 200, reply(`{"addresses":null}`, "stop"), ErrMalformed}, |
| 194 | {"a missing field", 200, reply(`{"addresses":[{"text":"`+canary+`"}]}`, "stop"), ErrMalformed}, |
| 195 | // dec.More() is not an end-of-input check; the first version |
| 196 | // accepted both of these. |
| 197 | {"a stray closing brace", 200, reply(`{"addresses":[{"passage":0,"text":"`+canary+`"}]}}`, "stop"), ErrMalformed}, |
| 198 | {"a stray closing bracket", 200, reply(`{"addresses":[]}]`, "stop"), ErrMalformed}, |
| 199 | // encoding/json folds "ſ" to "s"; strings.ToLower does not. This |
| 200 | // decoded as passage 1 through the first version's duplicate check. |
| 201 | {"a long-s duplicate", 200, reply(`{"addresses":[{"passage":0,"pa\u017f\u017fage":1,"text":"`+canary+`"}]}`, "stop"), ErrMalformed}, |
| 202 | {"a differently-cased key", 200, reply(`{"addresses":[{"PASSAGE":0,"text":"`+canary+`"}]}`, "stop"), ErrMalformed}, |
| 203 | // The envelope, not the content: the last duplicate wins, which |
| 204 | // would turn a truncation into a clean stop. |
| 205 | {"a duplicate finish_reason", 200, `{"choices":[{"message":{"content":"{\"addresses\":[]}"},"finish_reason":"length","finish_reason":"stop"}]}`, ErrMalformed}, |
| 206 | {"two choices", 200, `{"choices":[{"message":{"content":"{\"addresses\":[]}"},"finish_reason":"stop"},{"message":{"content":"{\"addresses\":[]}"},"finish_reason":"stop"}]}`, ErrMalformed}, |
| 207 | } { |
| 208 | t.Run(c.name, func(t *testing.T) { |
| 209 | s := newStub(t, c.status, c.body) |
| 210 | got, err := s.client().ExtractAddresses(context.Background(), []string{"x"}) |
| 211 | if !errors.Is(err, c.want) { |
| 212 | t.Fatalf("err = %v, want %v (got %+v)", err, c.want, got) |
| 213 | } |
| 214 | if got != nil { |
| 215 | t.Errorf("returned %+v alongside an error; a failure yields nothing", got) |
| 216 | } |
| 217 | assertNoCanary(t, err) |
| 218 | }) |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | // §6.5's deadline, reused. A slow provider must not hold the page. |
| 223 | func TestTheDeadlineHolds(t *testing.T) { |
| 224 | // Against a literal, not against Deadline itself: comparing the |
| 225 | // elapsed time to Deadline+slack passed when a review raised the |
| 226 | // constant to two seconds. |
| 227 | if Deadline != 800*time.Millisecond { |
| 228 | t.Fatalf("Deadline = %v; §8.9 and §6.5 say 800ms", Deadline) |
| 229 | } |
| 230 | s := newStub(t, 200, reply(`{"addresses":[]}`, "stop")) |
| 231 | s.delay = 5 * time.Second |
| 232 | start := time.Now() |
| 233 | _, err := s.client().ExtractAddresses(context.Background(), []string{"x"}) |
| 234 | // DeadlineExceeded specifically: the store logs a deadline and stays |
| 235 | // silent on a cancellation, so a deadline reported as Canceled is an |
| 236 | // outage nobody hears about. A review swapped them and this stayed |
| 237 | // green while it checked only err != nil. |
| 238 | if !errors.Is(err, context.DeadlineExceeded) { |
| 239 | t.Fatalf("err = %v, want context.DeadlineExceeded", err) |
| 240 | } |
| 241 | if took := time.Since(start); took > 1200*time.Millisecond { |
| 242 | t.Errorf("took %v; the deadline is 800ms", took) |
| 243 | } |
| 244 | } |
| 245 | |
| 246 | // An abandoned keystroke cancels the call rather than paying for it. |
| 247 | func TestCancellationReachesTheProvider(t *testing.T) { |
| 248 | s := newStub(t, 200, reply(`{"addresses":[]}`, "stop")) |
| 249 | s.delay = 5 * time.Second |
| 250 | ctx, cancel := context.WithCancel(context.Background()) |
| 251 | go func() { time.Sleep(50 * time.Millisecond); cancel() }() |
| 252 | start := time.Now() |
| 253 | if _, err := s.client().ExtractAddresses(ctx, []string{"x"}); !errors.Is(err, context.Canceled) { |
| 254 | t.Fatalf("err = %v, want context.Canceled", err) |
| 255 | } |
| 256 | if took := time.Since(start); took > 400*time.Millisecond { |
| 257 | t.Errorf("took %v after cancel", took) |
| 258 | } |
| 259 | // **At the listener.** The returned error proved only that the CLIENT |
| 260 | // stopped waiting; a review detached the request so it kept running in |
| 261 | // the background and this test stayed green. The server seeing its |
| 262 | // request context end is what shows the provider stopped too. |
| 263 | select { |
| 264 | case <-s.done: |
| 265 | case <-time.After(time.Second): |
| 266 | t.Error("the provider never saw the request cancelled; it is still paying for an abandoned keystroke") |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | // **What pays for this package's exemption from the outbound-network |
| 271 | // gate.** Decision 13 makes TensorX the only provider; this makes the |
| 272 | // client unable to reach anything else, whatever its base URL says — a |
| 273 | // mis-set constant, a future config field, a redirect. |
| 274 | // |
| 275 | // Each subtest counts requests at the stub, so "refused" means nothing |
| 276 | // reached the listener, and not merely that an error came back from |
| 277 | // somewhere after a request had already left. |
| 278 | // |
| 279 | // The different-host case uses "localhost" against a server pinned to |
| 280 | // "127.0.0.1": same scheme, same port, a live listener, so ONLY the host |
| 281 | // pin can refuse it. notion's first version of this test used an address |
| 282 | // that also failed the scheme check, and deleting the host pin left it |
| 283 | // green — a neighbouring error path supplying the failure. |
| 284 | func TestClientDialsOnlyTensorX(t *testing.T) { |
| 285 | t.Run("New is pinned to TensorX, https, port 443, no proxy", func(t *testing.T) { |
| 286 | c, err := New("k") |
| 287 | if err != nil { |
| 288 | t.Fatal(err) |
| 289 | } |
| 290 | pin := c.hc.Transport.(*pinnedTransport) |
| 291 | if c.base != BaseURL || pin.scheme != "https" || pin.authority != "api.tensorx.ai:443" { |
| 292 | t.Errorf("New dials %q pinned to %s://%s", c.base, pin.scheme, pin.authority) |
| 293 | } |
| 294 | // Structural, because behaviour cannot show it here: Go never |
| 295 | // proxies a request to 127.0.0.1, so an httptest server cannot |
| 296 | // tell a proxy-honouring transport from this one. A review showed |
| 297 | // the production constructor sending CONNECT to an HTTPS_PROXY |
| 298 | // listener while the host pin passed. |
| 299 | if tr, ok := pin.next.(*http.Transport); !ok || tr.Proxy != nil { |
| 300 | t.Errorf("the transport honours environment proxies: %T", pin.next) |
| 301 | } |
| 302 | }) |
| 303 | |
| 304 | t.Run("New refuses an empty credential", func(t *testing.T) { |
| 305 | if _, err := New(""); !errors.Is(err, ErrMissingKey) { |
| 306 | t.Errorf("err = %v, want ErrMissingKey", err) |
| 307 | } |
| 308 | }) |
| 309 | |
| 310 | // The first version pinned the host alone; another listener on the |
| 311 | // same host took the body, and ":8443" in BaseURL left every test green. |
| 312 | t.Run("another port on the same host is refused before dialling", func(t *testing.T) { |
| 313 | s := newStub(t, 200, reply(`{"addresses":[]}`, "stop")) |
| 314 | other := newStub(t, 200, reply(`{"addresses":[]}`, "stop")) |
| 315 | c := s.client() |
| 316 | c.base = other.srv.URL // same 127.0.0.1, different port |
| 317 | if _, err := c.ExtractAddresses(context.Background(), []string{"x"}); !errors.Is(err, ErrForbiddenHost) { |
| 318 | t.Fatalf("err = %v, want ErrForbiddenHost", err) |
| 319 | } |
| 320 | if n := other.calls.Load() + s.calls.Load(); n != 0 { |
| 321 | t.Errorf("%d requests arrived; the passages left", n) |
| 322 | } |
| 323 | }) |
| 324 | |
| 325 | t.Run("userinfo in the URL is refused", func(t *testing.T) { |
| 326 | s := newStub(t, 200, reply(`{"addresses":[]}`, "stop")) |
| 327 | c := s.client() |
| 328 | c.base = strings.Replace(s.srv.URL, "http://", "http://user:pw@", 1) |
| 329 | if _, err := c.ExtractAddresses(context.Background(), []string{"x"}); !errors.Is(err, ErrForbiddenHost) { |
| 330 | t.Fatalf("err = %v, want ErrForbiddenHost", err) |
| 331 | } |
| 332 | }) |
| 333 | |
| 334 | t.Run("another host is refused before dialling", func(t *testing.T) { |
| 335 | s := newStub(t, 200, reply(`{"addresses":[]}`, "stop")) |
| 336 | c := s.client() |
| 337 | c.base = strings.Replace(s.srv.URL, "127.0.0.1", "localhost", 1) |
| 338 | if c.base == s.srv.URL { |
| 339 | t.Fatal("could not build a different-host URL; this subtest proves nothing") |
| 340 | } |
| 341 | if _, err := c.ExtractAddresses(context.Background(), []string{"x"}); !errors.Is(err, ErrForbiddenHost) { |
| 342 | t.Fatalf("err = %v, want ErrForbiddenHost", err) |
| 343 | } |
| 344 | if n := s.calls.Load(); n != 0 { |
| 345 | t.Errorf("the stub received %d requests; the passages left", n) |
| 346 | } |
| 347 | }) |
| 348 | |
| 349 | t.Run("another scheme is refused before dialling", func(t *testing.T) { |
| 350 | s := newStub(t, 200, reply(`{"addresses":[]}`, "stop")) |
| 351 | c := s.client() |
| 352 | c.base = strings.Replace(s.srv.URL, "http://", "https://", 1) |
| 353 | if _, err := c.ExtractAddresses(context.Background(), []string{"x"}); !errors.Is(err, ErrForbiddenHost) { |
| 354 | t.Fatalf("err = %v, want ErrForbiddenHost", err) |
| 355 | } |
| 356 | if n := s.calls.Load(); n != 0 { |
| 357 | t.Errorf("the stub received %d requests", n) |
| 358 | } |
| 359 | }) |
| 360 | |
| 361 | // A redirect is refused outright, even to the same host: following it |
| 362 | // would resend the body — every passage — and, same-host, the key. |
| 363 | t.Run("a redirect is refused", func(t *testing.T) { |
| 364 | var hits atomic.Int32 |
| 365 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 366 | if hits.Add(1) == 1 { |
| 367 | // The Location is provider-chosen bytes. A review returned |
| 368 | // this exact shape and the first version's error quoted it. |
| 369 | http.Redirect(w, r, "/"+canary, http.StatusTemporaryRedirect) |
| 370 | return |
| 371 | } |
| 372 | _, _ = io.WriteString(w, reply(`{"addresses":[]}`, "stop")) |
| 373 | })) |
| 374 | t.Cleanup(srv.Close) |
| 375 | c, err := newClient("k", srv.URL) |
| 376 | if err != nil { |
| 377 | t.Fatal(err) |
| 378 | } |
| 379 | _, err = c.ExtractAddresses(context.Background(), []string{"x"}) |
| 380 | if !errors.Is(err, ErrForbiddenHost) { |
| 381 | t.Fatalf("err = %v, want ErrForbiddenHost", err) |
| 382 | } |
| 383 | assertNoCanary(t, err) |
| 384 | if n := hits.Load(); n != 1 { |
| 385 | t.Errorf("server saw %d requests; the redirect was followed", n) |
| 386 | } |
| 387 | }) |
| 388 | } |
| 389 | |
| 390 | // A response the HTTP layer cannot parse is a transport failure, and the |
| 391 | // bytes it choked on are not in the error. Go's own header parser quotes |
| 392 | // what it failed on; a review planted a canary in a malformed header and |
| 393 | // the first version returned it. Served from a raw socket, because an |
| 394 | // http.Server will not write a malformed header. |
| 395 | func TestAMalformedResponseCarriesNoContent(t *testing.T) { |
| 396 | ln, err := net.Listen("tcp", "127.0.0.1:0") |
| 397 | if err != nil { |
| 398 | t.Fatal(err) |
| 399 | } |
| 400 | t.Cleanup(func() { ln.Close() }) |
| 401 | go func() { |
| 402 | conn, err := ln.Accept() |
| 403 | if err != nil { |
| 404 | return |
| 405 | } |
| 406 | defer conn.Close() |
| 407 | // Consume the request line and headers, then answer badly. |
| 408 | br := bufio.NewReader(conn) |
| 409 | for { |
| 410 | line, err := br.ReadString('\n') |
| 411 | if err != nil || line == "\r\n" { |
| 412 | break |
| 413 | } |
| 414 | } |
| 415 | _, _ = conn.Write([]byte("HTTP/1.1 200 OK\r\n" + canary + " no colon here\r\n\r\n")) |
| 416 | }() |
| 417 | c, err := newClient("k", "http://"+ln.Addr().String()) |
| 418 | if err != nil { |
| 419 | t.Fatal(err) |
| 420 | } |
| 421 | _, err = c.ExtractAddresses(context.Background(), []string{"x"}) |
| 422 | if !errors.Is(err, ErrTransport) { |
| 423 | t.Fatalf("err = %v, want ErrTransport", err) |
| 424 | } |
| 425 | assertNoCanary(t, err) |
| 426 | } |
| 427 | |
| 428 | // rejectAmbiguous is called DIRECTLY here, because through |
| 429 | // ExtractAddresses two of its checks are unreachable: the content's |
| 430 | // exact-key check rejects "paſſage" before the duplicate check runs, and |
| 431 | // decodeAddresses has its own end-of-input check. Mutating either inside |
| 432 | // rejectAmbiguous left every other test green (found 2026-09-27). They |
| 433 | // still matter — for the ENVELOPE, whose keys are not enumerated — so |
| 434 | // they are pinned where they can be reached (AGENTS.md: make a defence |
| 435 | // reachable or delete it). |
| 436 | func TestRejectAmbiguousDirectly(t *testing.T) { |
| 437 | for _, c := range []struct { |
| 438 | name, in string |
| 439 | exact map[string]bool |
| 440 | ok bool |
| 441 | }{ |
| 442 | {"a clean document", `{"a":1,"b":[{"c":2}]}`, nil, true}, |
| 443 | {"a stray closing brace", `{"a":1}}`, nil, false}, |
| 444 | {"a stray closing bracket", `{"a":1}]`, nil, false}, |
| 445 | {"two documents", `{"a":1} {"a":2}`, nil, false}, |
| 446 | {"a plain duplicate", `{"a":1,"a":2}`, nil, false}, |
| 447 | {"a case duplicate", `{"a":1,"A":2}`, nil, false}, |
| 448 | // Go's decoder folds ſ to s; strings.ToLower does not. With no |
| 449 | // exact-key list — the envelope's case — only EqualFold sees it. |
| 450 | {"a long-s duplicate", `{"finish_reason":"length","finiſh_reaſon":"stop"}`, nil, false}, |
| 451 | {"a nested long-s duplicate", `{"x":{"passage":0,"paſſage":1}}`, nil, false}, |
| 452 | {"an exact key", `{"text":"a"}`, contentKeys, true}, |
| 453 | {"a differently-cased key", `{"TEXT":"a"}`, contentKeys, false}, |
| 454 | } { |
| 455 | if err := rejectAmbiguous([]byte(c.in), c.exact); (err == nil) != c.ok { |
| 456 | t.Errorf("%s: err = %v, want ok=%v", c.name, err, c.ok) |
| 457 | } |
| 458 | } |
| 459 | } |
| 460 | |
| 461 | // And the long-s duplicate end to end in the envelope, where it would |
| 462 | // otherwise turn a truncation into a clean stop. |
| 463 | func TestALongSEnvelopeDuplicateIsRefused(t *testing.T) { |
| 464 | body := `{"choices":[{"message":{"content":"{\"addresses\":[]}"},"finish_reason":"length","finiſh_reaſon":"stop"}]}` |
| 465 | s := newStub(t, 200, body) |
| 466 | if _, err := s.client().ExtractAddresses(context.Background(), []string{"x"}); !errors.Is(err, ErrMalformed) { |
| 467 | t.Errorf("err = %v, want ErrMalformed: the duplicate would have decoded as a clean stop", err) |
| 468 | } |
| 469 | } |
| 470 | |
| 471 | // **The standard library's own logger is a content path too.** net/http |
| 472 | // reads idle pooled connections, and bytes arriving on one unsolicited |
| 473 | // are reported with log.Printf, quoting them — bypassing every sanitised |
| 474 | // error in this package. A review reproduced it over real TLS: a valid |
| 475 | // response, then passage text on the same connection, then that text on |
| 476 | // stderr. The client now disables keep-alives, so there is no idle |
| 477 | // connection to read. |
| 478 | // |
| 479 | // Captures the STANDARD logger, because that is the channel. Served from |
| 480 | // a raw socket that answers correctly and then keeps talking. |
| 481 | func TestUnsolicitedBytesDoNotReachTheStandardLogger(t *testing.T) { |
| 482 | var logged bytes.Buffer |
| 483 | log.SetOutput(&logged) |
| 484 | t.Cleanup(func() { log.SetOutput(os.Stderr) }) |
| 485 | |
| 486 | ln, err := net.Listen("tcp", "127.0.0.1:0") |
| 487 | if err != nil { |
| 488 | t.Fatal(err) |
| 489 | } |
| 490 | t.Cleanup(func() { ln.Close() }) |
| 491 | served := make(chan struct{}) |
| 492 | go func() { |
| 493 | conn, err := ln.Accept() |
| 494 | if err != nil { |
| 495 | return |
| 496 | } |
| 497 | defer conn.Close() |
| 498 | br := bufio.NewReader(conn) |
| 499 | for { |
| 500 | line, err := br.ReadString('\n') |
| 501 | if err != nil || line == "\r\n" { |
| 502 | break |
| 503 | } |
| 504 | } |
| 505 | body := reply(`{"addresses":[]}`, "stop") |
| 506 | fmt.Fprintf(conn, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s", len(body), body) |
| 507 | // A moment for the client to finish and pool the connection, then |
| 508 | // talk unasked. |
| 509 | time.Sleep(50 * time.Millisecond) |
| 510 | _, _ = conn.Write([]byte(canary + " unsolicited team content")) |
| 511 | time.Sleep(150 * time.Millisecond) |
| 512 | close(served) |
| 513 | }() |
| 514 | |
| 515 | c, err := newClient("k", "http://"+ln.Addr().String()) |
| 516 | if err != nil { |
| 517 | t.Fatal(err) |
| 518 | } |
| 519 | if _, err := c.ExtractAddresses(context.Background(), []string{"x"}); err != nil { |
| 520 | t.Fatalf("the valid response failed: %v", err) |
| 521 | } |
| 522 | <-served |
| 523 | time.Sleep(50 * time.Millisecond) // let any read loop run |
| 524 | if strings.Contains(logged.String(), canary) { |
| 525 | t.Errorf("the standard logger carries provider bytes: %q", logged.String()) |
| 526 | } |
| 527 | } |
| 528 | |
| 529 | // **An untrusted certificate receives nothing, and a trusted one works.** |
| 530 | // A review set InsecureSkipVerify on the transport and every test stayed |
| 531 | // green, because every stub spoke plain HTTP. Both halves: without the |
| 532 | // trusted half, a client that refused ALL TLS would pass. |
| 533 | func TestTLSIsVerified(t *testing.T) { |
| 534 | var hits atomic.Int32 |
| 535 | srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 536 | hits.Add(1) |
| 537 | _, _ = io.WriteString(w, reply(`{"addresses":[]}`, "stop")) |
| 538 | })) |
| 539 | t.Cleanup(srv.Close) |
| 540 | |
| 541 | untrusted, err := newClient("k", srv.URL) // system roots: the test CA is not among them |
| 542 | if err != nil { |
| 543 | t.Fatal(err) |
| 544 | } |
| 545 | if _, err := untrusted.ExtractAddresses(context.Background(), []string{"x"}); !errors.Is(err, ErrTransport) { |
| 546 | t.Fatalf("err = %v, want ErrTransport", err) |
| 547 | } |
| 548 | if n := hits.Load(); n != 0 { |
| 549 | t.Fatalf("%d requests reached a server whose certificate is not trusted", n) |
| 550 | } |
| 551 | |
| 552 | pool := x509.NewCertPool() |
| 553 | pool.AddCert(srv.Certificate()) |
| 554 | trusted, err := newClientWithRoots("k", srv.URL, pool) |
| 555 | if err != nil { |
| 556 | t.Fatal(err) |
| 557 | } |
| 558 | if _, err := trusted.ExtractAddresses(context.Background(), []string{"x"}); err != nil { |
| 559 | t.Fatalf("a trusted certificate failed: %v", err) |
| 560 | } |
| 561 | if n := hits.Load(); n != 1 { |
| 562 | t.Errorf("hits = %d, want 1", n) |
| 563 | } |
| 564 | } |
| 565 | |