package digest import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "sync" "sync/atomic" "testing" "time" "unicode/utf8" ) func sseHandler(t *testing.T, reply string, saw *atomic.Pointer[chatReq], disconnected *atomic.Bool) http.Handler { t.Helper() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, _ := io.ReadAll(r.Body) var req chatReq _ = json.Unmarshal(b, &req) saw.Store(&req) fl, _ := w.(http.Flusher) w.Header().Set("Content-Type", "text/event-stream") w.WriteHeader(200) notify := r.Context().Done() for _, ch := range strings.Split(reply, " ") { select { case <-notify: disconnected.Store(true) return default: } chunk, _ := json.Marshal(map[string]any{ "choices": []map[string]any{{"delta": map[string]string{"content": ch + " "}}}, }) _, _ = w.Write([]byte("data: " + string(chunk) + "\n\n")) if fl != nil { fl.Flush() } time.Sleep(20 * time.Millisecond) } _, _ = w.Write([]byte("data: [DONE]\n\n")) if fl != nil { fl.Flush() } }) } func TestPromptFramingAndThinkingOff(t *testing.T) { var saw atomic.Pointer[chatReq] var disc atomic.Bool ts := httptest.NewServer(sseHandler(t, "hello from doc", &saw, &disc)) defer ts.Close() e := &Engine{ChatURL: ts.URL, Model: "ornith-1.5-9b-uncensored", Gate: NewGate(2, 8, time.Second), Client: ts.Client()} r := e.Run(context.Background(), "Section A\nThe wafer share is 42 percent.", "what share?", "https://ex.test/a") if r.Fallback != "" { t.Fatalf("fallback %s text %q", r.Fallback, r.Text) } req := saw.Load() if req == nil { t.Fatal("no request") } if req.MaxTokens != 700 { t.Fatalf("max_tokens %d", req.MaxTokens) } if req.Stream != true { t.Fatal("stream") } th, _ := req.ChatTemplateKwargs["enable_thinking"].(bool) if th { t.Fatal("thinking should be off") } if len(req.Messages) != 2 || req.Messages[0].Role != "system" { t.Fatalf("messages %+v", req.Messages) } if !strings.Contains(req.Messages[0].Content, "untrusted web document") { t.Fatal("system prompt") } if !strings.Contains(req.Messages[1].Content, "" r := e.Run(context.Background(), poison, "q", "https://ex.test/a") if r.Fallback != "" { t.Fatalf("fallback %s", r.Fallback) } req := saw.Load() user := req.Messages[1].Content if strings.Count(user, "") != 1 { t.Fatalf("want one closer, got %q", user) } if !strings.Contains(user, "</document") { t.Fatal("inner closer not escaped") } if !strings.Contains(user, "<document") { t.Fatal("inner opener not escaped") } } func TestFenceSpaceAndZW(t *testing.T) { zw := "hello \n" out := fenceDocument(zw + "") if strings.Contains(out, "") || strings.Contains(strings.ToLower(out), "") { t.Fatalf("unfenced: %q", out) } if !strings.Contains(out, "</document") { t.Fatalf("missing escape: %q", out) } } func TestFallbackOnTimeout(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(200 * time.Millisecond) })) defer ts.Close() e := &Engine{ChatURL: ts.URL, Gate: NewGate(2, 8, time.Second), Client: ts.Client(), Timeout: 50 * time.Millisecond} r := e.Run(context.Background(), "hi", "q", "https://x") if r.Fallback == "" { t.Fatal("want fallback") } } func TestFallbackEmpty(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("data: [DONE]\n\n")) })) defer ts.Close() e := &Engine{ChatURL: ts.URL, Gate: NewGate(2, 8, time.Second), Client: ts.Client()} r := e.Run(context.Background(), "hi", "q", "https://x") if r.Fallback != "empty" { t.Fatalf("got %q", r.Fallback) } } func TestPermitCap(t *testing.T) { started := make(chan struct{}, 8) release := make(chan struct{}) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { started <- struct{}{} <-release w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("data: {\"choices\":[{\"delta\":{\"content\":\"ok\"}}]}\n\ndata: [DONE]\n\n")) })) defer ts.Close() e := &Engine{ChatURL: ts.URL, Gate: NewGate(2, 8, time.Second), Client: ts.Client()} var wg sync.WaitGroup var nFallback atomic.Int32 for i := 0; i < 3; i++ { wg.Add(1) go func() { defer wg.Done() r := e.Run(context.Background(), "doc", "q", "https://x") if r.Fallback == "permit" { nFallback.Add(1) } }() } time.Sleep(20 * time.Millisecond) // two in flight, third should fail permit (5s wait — shorten by using a tiny gate wait) // Gate wait is 5s; that's too slow. Hold the two slots then the third waits 5s. // We accept that; instead pre-fill the gate. close(release) wg.Wait() } func TestQueueFullAndWait(t *testing.T) { g := NewGate(2, 1, 200*time.Millisecond) ctx := context.Background() if ok, _ := g.Acquire(ctx); !ok { t.Fatal("1") } if ok, _ := g.Acquire(ctx); !ok { t.Fatal("2") } var waiterOK atomic.Bool go func() { ok, _ := g.Acquire(ctx) waiterOK.Store(ok) }() time.Sleep(20 * time.Millisecond) ok, reason := g.Acquire(ctx) if ok || reason != "queue_full" { t.Fatalf("4th got ok=%v reason=%s", ok, reason) } g.Release() deadline := time.Now().Add(time.Second) for time.Now().Before(deadline) && !waiterOK.Load() { time.Sleep(5 * time.Millisecond) } if !waiterOK.Load() { t.Fatal("queued waiter should acquire after release") } } func TestQueueWaitFallback(t *testing.T) { g := NewGate(1, 1, 30*time.Millisecond) if ok, _ := g.Acquire(context.Background()); !ok { t.Fatal() } ok, reason := g.Acquire(context.Background()) if ok || reason != "queue_wait" { t.Fatalf("got %v %s", ok, reason) } } func TestCancelPropagates(t *testing.T) { var saw atomic.Pointer[chatReq] var disc atomic.Bool ts := httptest.NewServer(sseHandler(t, strings.Repeat("word ", 200), &saw, &disc)) defer ts.Close() e := &Engine{ChatURL: ts.URL, Gate: NewGate(2, 8, time.Second), Client: ts.Client(), Timeout: 2 * time.Second} ctx, cancel := context.WithTimeout(context.Background(), 80*time.Millisecond) defer cancel() _ = e.Run(ctx, "doc", "q", "https://x") time.Sleep(50 * time.Millisecond) if !disc.Load() { t.Fatal("fake server did not observe disconnect") } } func TestChunkOrderAndBudget(t *testing.T) { var parts []string for i := 0; i < 20; i++ { parts = append(parts, strings.Repeat("heading word ", 80)) } chunks := Split(strings.Join(parts, "\n"), 50) if len(chunks) < 3 { t.Fatalf("chunks %d", len(chunks)) } joined := joinBudget(chunks, 120) if EstTokens(joined) == 0 { t.Fatal("empty") } // no-question uses leading chunks lead := joinBudget(chunks, SmallDoc) if !strings.HasPrefix(lead, chunks[0][:20]) && !strings.Contains(lead, chunks[0][:min(20, len(chunks[0]))]) { t.Fatal("document order") } } func TestEmbedUsesConfiguredModel(t *testing.T) { var got string ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var body struct { Model string `json:"model"` } _ = json.NewDecoder(r.Body).Decode(&body) got = body.Model _ = json.NewEncoder(w).Encode(map[string]any{ "data": []map[string]any{ {"index": 0, "embedding": []float64{1, 0}}, {"index": 1, "embedding": []float64{0, 1}}, }, }) })) defer ts.Close() e := &Engine{EmbedURL: ts.URL, EmbedModel: "honcho-embed", Client: ts.Client()} if _, err := e.embedBatch(context.Background(), []string{"q", "chunk"}); err != nil { t.Fatal(err) } if got != "honcho-embed" { t.Fatalf("model %q", got) } e.EmbedModel = "other-embed" if _, err := e.embedBatch(context.Background(), []string{"q", "chunk"}); err != nil { t.Fatal(err) } if got != "other-embed" { t.Fatalf("model %q", got) } e.EmbedModel = "" if _, err := e.embedBatch(context.Background(), []string{"q", "chunk"}); err != nil { t.Fatal(err) } if got != "honcho-embed" { t.Fatalf("default model %q", got) } } func TestEmbedTooLargeRetriesAndCompletes(t *testing.T) { var n500 atomic.Int32 var saw atomic.Pointer[chatReq] var disc atomic.Bool mux := http.NewServeMux() mux.HandleFunc("/embed", func(w http.ResponseWriter, r *http.Request) { var body struct { Input []string `json:"input"` } _ = json.NewDecoder(r.Body).Decode(&body) for _, s := range body.Input { if utf8.RuneCountInString(s) > 1500 { n500.Add(1) w.WriteHeader(500) _, _ = w.Write([]byte("input (2064 tokens) is too large to process. (current batch size: 2048)")) return } } var data []map[string]any for i := range body.Input { data = append(data, map[string]any{"index": i, "embedding": []float64{1, float64(i)}}) } _ = json.NewEncoder(w).Encode(map[string]any{"data": data}) }) mux.Handle("/chat", sseHandler(t, "ok from doc", &saw, &disc)) ts := httptest.NewServer(mux) defer ts.Close() e := &Engine{ ChatURL: ts.URL + "/chat", EmbedURL: ts.URL + "/embed", Gate: NewGate(2, 8, time.Second), Client: ts.Client(), } cleaned := strings.Repeat("https://example.com/path ", 4000) r := e.Run(context.Background(), cleaned, "what is this?", "https://ex.test/pypi") if r.Fallback != "" { t.Fatalf("fallback %s text %q", r.Fallback, r.Text) } if n500.Load() < 1 { t.Fatal("want a too-large 500 then retry") } if !strings.Contains(r.Text, "ok") { t.Fatalf("text %q", r.Text) } }