Files
sift/internal/search/sources_test.go
T
kyle 8b26776cdd feat(search): P3.1 filters and multi-query merge
Native include/exclude domains, time_range, topic, exact_match, min_score,
and up to 4 queries fused with RRF. Honesty notes stay off the Warn/fail
path. Tavily keeps include_answer false and sets auto_parameters false.
Cache key is canonical JSON. CLI flags and skill recipe included.
2026-09-29 15:30:19 -07:00

153 lines
4.7 KiB
Go

package search
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"git.wntrmute.dev/kyle/sift/internal/search/dial"
)
func TestTavilyOutboundAndDropsAnswer(t *testing.T) {
var gotBody []byte
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("method %s", r.Method)
}
if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
t.Error("missing bearer")
}
gotBody, _ = io.ReadAll(r.Body)
_, _ = w.Write([]byte(`{"answer":"SECRET","results":[{"url":"https://ex.com/a","title":"A","content":"snip","score":0.9}],"usage":{"credits":1}}`))
}))
defer ts.Close()
c, err := dial.New(ts.URL, dial.Options{Loopback: true, Timeout: time.Second})
if err != nil {
t.Fatal(err)
}
out := (&Tavily{Client: c, Key: "test-key"}).Search(context.Background(), "q", 5, Filter{})
if out.Warn != "" {
t.Fatal(out.Warn)
}
var req map[string]any
if err := json.Unmarshal(gotBody, &req); err != nil {
t.Fatal(err)
}
if req["include_answer"] != false || req["include_raw_content"] != false || req["include_images"] != false || req["search_depth"] != "basic" {
t.Fatalf("%+v", req)
}
if req["auto_parameters"] != false {
t.Fatalf("auto_parameters %+v", req["auto_parameters"])
}
if strings.Contains(string(gotBody), "extract") || strings.Contains(string(gotBody), "crawl") {
t.Fatal(string(gotBody))
}
if len(out.Hits) != 1 || out.Hits[0].URL != "https://ex.com/a" {
t.Fatalf("%+v", out.Hits)
}
for _, h := range out.Hits {
if strings.Contains(h.Snippet, "SECRET") || strings.Contains(h.Title, "SECRET") {
t.Fatal("answer leaked into hit")
}
}
}
func TestSearxNGMapsAndSorts(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/search" || r.URL.Query().Get("format") != "json" {
t.Errorf("path %s", r.URL.String())
}
_, _ = w.Write([]byte(`{"results":[
{"url":"https://b.example/","title":"b","content":"bb","score":0.1},
{"url":"https://a.example/","title":"a","content":"aa","score":0.9}
]}`))
}))
defer ts.Close()
c, err := dial.New(ts.URL, dial.Options{Loopback: true, Timeout: time.Second})
if err != nil {
t.Fatal(err)
}
out := (&SearxNG{Client: c}).Search(context.Background(), "q", 8, Filter{})
if out.Warn != "" || len(out.Hits) != 2 || out.Hits[0].Title != "a" {
t.Fatalf("%+v", out)
}
}
func TestBraveMapsSources(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Subscription-Token") == "" {
t.Error("token")
}
_, _ = w.Write([]byte(`{"grounding":"ignore me","sources":[{"url":"https://s.example/x","title":"S","snippet":"hello"}]}`))
}))
defer ts.Close()
c, err := dial.New(ts.URL, dial.Options{Loopback: true, Timeout: time.Second})
if err != nil {
t.Fatal(err)
}
out := (&BraveLLM{Client: c, Key: "k"}).Search(context.Background(), "q", 5, Filter{})
if len(out.Hits) != 1 || out.Hits[0].URL != "https://s.example/x" {
t.Fatalf("%+v", out)
}
if strings.Contains(out.Hits[0].Snippet, "ignore") {
t.Fatal("grounding used as snippet")
}
}
func TestBraveMapsGroundingGeneric(t *testing.T) {
raw, err := os.ReadFile("../../tests/testdata/brave_llm_context.json")
if err != nil {
t.Fatal(err)
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(raw)
}))
defer ts.Close()
c, err := dial.New(ts.URL, dial.Options{Loopback: true, Timeout: time.Second})
if err != nil {
t.Fatal(err)
}
out := (&BraveLLM{Client: c, Key: "k"}).Search(context.Background(), "q", 5, Filter{})
if len(out.Hits) != 3 {
t.Fatalf("hits %d %+v", len(out.Hits), out.Hits)
}
if out.Hits[0].URL != "https://docs.tavily.com/documentation/api-reference/endpoint/search" {
t.Fatalf("first %+v", out.Hits[0])
}
if !strings.Contains(out.Hits[0].Snippet, "max_results") {
t.Fatalf("snippet %q", out.Hits[0].Snippet)
}
var urls []string
for _, h := range out.Hits {
urls = append(urls, h.URL)
}
joined := strings.Join(urls, " ")
if !strings.Contains(joined, "orphan.example") {
t.Fatalf("sources map missed: %v", urls)
}
}
func TestTavilyBadJSONIsError(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`not-json`))
}))
defer ts.Close()
c, err := dial.New(ts.URL, dial.Options{Loopback: true, Timeout: time.Second})
if err != nil {
t.Fatal(err)
}
out := (&Tavily{Client: c, Key: "k"}).Search(context.Background(), "q", 5, Filter{})
if out.Warn != "tavily: error" {
t.Fatalf("warn %q", out.Warn)
}
if len(out.Hits) != 0 || out.Credits != 0 {
t.Fatalf("%+v", out)
}
}