Ctrl K
ComunidadGuías…

internal/responsecache/simple_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package responsecache

import (
	"bytes"
	"context"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/labstack/echo/v5"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/enterpilot/gomodel/internal/cache"
	"github.com/enterpilot/gomodel/internal/echotest"
)

// missStore is a cache.Store that never holds an entry, so every request
// takes the full miss path: lookup, provider call, capture and write.
type missStore struct{}

func (missStore) Get(context.Context, string) ([]byte, error)              { return nil, nil }
func (missStore) Set(context.Context, string, []byte, time.Duration) error { return nil }
func (missStore) Close() error                                             { return nil }

func BenchmarkHandleExactMiss(b *testing.B) {
	content := strings.Repeat("Summarize the quarterly report section by section. ", 700)
	body := []byte(`{"model":"gpt-4o","messages":[{"role":"system","content":"You are concise."},{"role":"user","content":"` + content + `"}],"temperature":0.2}`)
	mw := &ResponseCacheMiddleware{simple: newSimpleCacheMiddleware(missStore{}, time.Hour, nil)}
	defer mw.Close()
	ex := &blockingMissExchange{ctx: context.Background()}
	next := func() error { return nil }
	b.SetBytes(int64(len(body)))
	b.ReportAllocs()
	for b.Loop() {
		if err := mw.handle(ex, body, next); err != nil {
			b.Fatal(err)
		}
	}
}

// sseStreamOfSize builds a cacheable chat stream of at least size bytes as
// separate events, so a capture sees it arrive write by write.
func sseStreamOfSize(size int) [][]byte {
	event := []byte(`data: {"object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"` + strings.Repeat("x", 1000) + `"}}]}` + "\n\n")
	var events [][]byte
	for total := 0; total < size; total += len(event) {
		events = append(events, event)
	}
	return append(events, []byte("data: [DONE]\n\n"))
}

// sseStreamOfExactSize builds a cacheable chat stream of exactly size bytes,
// padding it with an SSE comment before the final [DONE] event.
func sseStreamOfExactSize(t *testing.T, size int) [][]byte {
	t.Helper()
	events := sseStreamOfSize(size - 4096)
	done := events[len(events)-1]
	events = events[:len(events)-1]
	padLen := size - len(bytes.Join(events, nil)) - len(done)
	require.GreaterOrEqual(t, padLen, 4)
	padding := []byte(": " + strings.Repeat(" ", padLen-4) + "\n\n")
	events = append(events, padding, done)
	require.Len(t, bytes.Join(events, nil), size)
	return events
}

func TestHandleRequest_CapturedStreamSizeLimit(t *testing.T) {
	largeJSON := []byte(`{"data":"` + strings.Repeat("x", maxCapturedStreamBytes+1024) + `"}`)
	tests := []struct {
		name       string
		stream     bool
		writes     [][]byte
		wantCached bool
	}{
		{name: "stream under the limit is cached", stream: true, writes: sseStreamOfSize(maxCapturedStreamBytes - 4096), wantCached: true},
		{name: "stream at exactly the limit is cached", stream: true, writes: sseStreamOfExactSize(t, maxCapturedStreamBytes), wantCached: true},
		{name: "stream over the limit is not cached", stream: true, writes: sseStreamOfSize(maxCapturedStreamBytes + 4096)},
		{name: "non-streaming body over the limit is still cached", writes: [][]byte{largeJSON}, wantCached: true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			store := cache.NewMapStore()
			mw := NewResponseCacheMiddlewareWithStore(store, time.Hour)
			t.Cleanup(func() { _ = mw.Close() })
			workflow := resolvedWorkflow("openai", "gpt-4")
			body := []byte(`{"model":"gpt-4","messages":[{"role":"user","content":"size limit"}]}`)
			contentType := "application/json"
			if tt.stream {
				body = []byte(`{"model":"gpt-4","stream":true,"messages":[{"role":"user","content":"size limit"}]}`)
				contentType = "text/event-stream"
			}
			want := bytes.Join(tt.writes, nil)
			calls := 0
			next := func(c *echo.Context) error {
				calls++
				c.Response().Header().Set("Content-Type", contentType)
				c.Response().WriteHeader(http.StatusOK)
				for _, w := range tt.writes {
					_, err := c.Response().Write(w)
					require.NoError(t, err)
				}
				return nil
			}

			first := driveHandleRequest(t, mw, workflow, body, nil, next)
			assert.Empty(t, first.Header().Get("X-Cache"))
			assert.Equal(t, want, first.Body.Bytes(), "the client receives the whole response")
			mw.simple.wg.Wait()

			second := driveHandleRequest(t, mw, workflow, body, nil, next)
			assert.Equal(t, want, second.Body.Bytes())
			if tt.wantCached {
				assert.Equal(t, CacheHeaderExact, second.Header().Get("X-Cache"))
				assert.Equal(t, 1, calls)
				return
			}
			assert.Empty(t, second.Header().Get("X-Cache"))
			assert.Equal(t, 2, calls)
		})
	}
}

func TestCaptureResponseForCache_NestedCapture(t *testing.T) {
	tests := []struct {
		name string
		// outerFirst writes the status through the outer capture before the
		// inner capture starts, so the inner one cannot reuse it.
		outerFirst bool
		wantShared bool
	}{
		{name: "untouched outer capture is reused", wantShared: true},
		{name: "outer capture already written to gets its own inner capture", outerFirst: true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c, rec := echotest.Post(t, "/v1/chat/completions", nil)
			var inner []byte
			outer, outerOK, err := captureResponseForCache(c, "/v1/chat/completions", "outer", func() error {
				if tt.outerFirst {
					c.Response().WriteHeader(http.StatusOK)
				}
				var innerOK bool
				var innerErr error
				inner, innerOK, innerErr = captureResponseForCache(c, "/v1/chat/completions", "inner", func() error {
					return c.JSON(http.StatusOK, map[string]string{"result": "ok"})
				})
				require.NoError(t, innerErr)
				require.True(t, innerOK)
				return nil
			})
			require.NoError(t, err)
			require.True(t, outerOK)
			require.NotEmpty(t, inner)
			require.NotEmpty(t, outer)

			assert.Equal(t, rec.Body.Bytes(), inner)
			assert.Equal(t, rec.Body.Bytes(), outer)
			if tt.wantShared {
				assert.Same(t, &outer[0], &inner[0], "both layers read one buffer")
				return
			}
			assert.NotSame(t, &outer[0], &inner[0])
		})
	}
}