ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
104 lines
3.2 KiB
Go
104 lines
3.2 KiB
Go
package llmrec
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestCaptureFromNilAndMissing(t *testing.T) {
|
|
var nilCtx context.Context // the transport reaches here on any unrecorded call
|
|
if CaptureFrom(nilCtx) != nil {
|
|
t.Fatal("nil context should yield no capture")
|
|
}
|
|
if CaptureFrom(context.Background()) != nil {
|
|
t.Fatal("bare context should yield no capture")
|
|
}
|
|
}
|
|
|
|
// A nil *Capture is the "recording off" path: the transport calls these on every
|
|
// request, so they must all be no-ops rather than panics.
|
|
func TestNilCaptureIsInert(t *testing.T) {
|
|
var c *Capture
|
|
c.SetRequest("body")
|
|
if got := c.RawRequest(); got != "" {
|
|
t.Fatalf("RawRequest()=%q want empty", got)
|
|
}
|
|
if got := c.RawResponse(); got != "" {
|
|
t.Fatalf("RawResponse()=%q want empty", got)
|
|
}
|
|
rc := io.NopCloser(strings.NewReader("x"))
|
|
if c.TeeResponse(200, rc) != rc {
|
|
t.Fatal("nil capture must pass the body through untouched")
|
|
}
|
|
}
|
|
|
|
func TestCaptureSingleAttemptKeepsBytesVerbatim(t *testing.T) {
|
|
ctx, c := NewCapture(context.Background())
|
|
if CaptureFrom(ctx) != c {
|
|
t.Fatal("capture not retrievable from its own context")
|
|
}
|
|
c.SetRequest(`{"model":"x"}`)
|
|
// Retries re-send identical bytes; only the first is kept.
|
|
c.SetRequest(`{"model":"ignored"}`)
|
|
|
|
const sse = "event: message_start\ndata: {\"type\":\"message_start\"}\n\nevent: message_stop\ndata: {}\n\n"
|
|
body := c.TeeResponse(200, io.NopCloser(strings.NewReader(sse)))
|
|
got, err := io.ReadAll(body)
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if string(got) != sse {
|
|
t.Fatal("tee altered the stream delivered to the caller")
|
|
}
|
|
if err := body.Close(); err != nil {
|
|
t.Fatalf("close: %v", err)
|
|
}
|
|
|
|
if c.RawRequest() != `{"model":"x"}` {
|
|
t.Fatalf("RawRequest()=%q", c.RawRequest())
|
|
}
|
|
// A lone attempt must stay byte-identical — no headers, no framing added, so
|
|
// it can be replayed as-is.
|
|
if c.RawResponse() != sse {
|
|
t.Fatalf("RawResponse()=%q want verbatim SSE", c.RawResponse())
|
|
}
|
|
}
|
|
|
|
// norma's doStream discards the bodies of retried attempts (retry.go closes them
|
|
// unread), so the capture is the only place a 429 body survives.
|
|
func TestCaptureRetriedAttemptsAreAllKept(t *testing.T) {
|
|
_, c := NewCapture(context.Background())
|
|
|
|
first := c.TeeResponse(429, io.NopCloser(strings.NewReader(`{"error":"rate_limit"}`)))
|
|
if _, err := io.ReadAll(first); err != nil {
|
|
t.Fatalf("read first: %v", err)
|
|
}
|
|
second := c.TeeResponse(200, io.NopCloser(strings.NewReader("data: ok\n")))
|
|
if _, err := io.ReadAll(second); err != nil {
|
|
t.Fatalf("read second: %v", err)
|
|
}
|
|
|
|
raw := c.RawResponse()
|
|
if !strings.Contains(raw, `{"error":"rate_limit"}`) {
|
|
t.Fatalf("dropped the retried attempt body: %q", raw)
|
|
}
|
|
if !strings.Contains(raw, "data: ok") {
|
|
t.Fatalf("dropped the final attempt body: %q", raw)
|
|
}
|
|
if !strings.Contains(raw, "attempt 1/2 — HTTP 429") || !strings.Contains(raw, "attempt 2/2 — HTTP 200") {
|
|
t.Fatalf("attempts not delimited: %q", raw)
|
|
}
|
|
if strings.Index(raw, "rate_limit") > strings.Index(raw, "data: ok") {
|
|
t.Fatal("attempts stored out of order")
|
|
}
|
|
}
|
|
|
|
func TestCaptureEmptyWhenNoHTTPHappened(t *testing.T) {
|
|
_, c := NewCapture(context.Background())
|
|
if c.RawRequest() != "" || c.RawResponse() != "" {
|
|
t.Fatal("unused capture should be empty")
|
|
}
|
|
}
|