package search import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "git.wntrmute.dev/kyle/sift/internal/search/dial" ) func TestTavilyFiltersOutbound(t *testing.T) { var gotBody []byte ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotBody, _ = io.ReadAll(r.Body) _, _ = w.Write([]byte(`{"answer":"SECRET","results":[{"url":"https://ex.com/a","title":"A","content":"snip","score":0.9,"published_date":"2026-09-01"}]}`)) })) defer ts.Close() c, err := dial.New(ts.URL, dial.Options{Loopback: true, Timeout: time.Second}) if err != nil { t.Fatal(err) } f := Filter{ IncludeDomains: []string{"ex.com"}, IncludeDomainsMode: "restrict", TimeRange: "month", Topic: "news", ExactMatch: true, } out := (&Tavily{Client: c, Key: "k"}).Search(context.Background(), `docs "phrase"`, 5, f) 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["auto_parameters"] != false || req["search_depth"] != "basic" { t.Fatalf("%+v", req) } if req["query"] != `docs "phrase"` { t.Fatalf("query rewritten: %v", req["query"]) } if req["exact_match"] != true { t.Fatalf("exact_match %v", req["exact_match"]) } if _, ok := req["start_date"]; ok { t.Fatal("time_range with dates") } if req["time_range"] != "month" || req["topic"] != "news" { t.Fatalf("%+v", req) } if req["include_published_date"] != true { t.Fatal("want include_published_date") } if _, ok := req["include_domains_mode"]; !ok { t.Fatal("mode missing") } if strings.Contains(string(gotBody), "extract") || strings.Contains(string(gotBody), "/crawl") { t.Fatal(string(gotBody)) } if len(out.Hits) != 1 || out.Hits[0].PublishedDate != "2026-09-01" { t.Fatalf("%+v", out.Hits) } } func TestTavilyModeOmittedWithoutInclude(t *testing.T) { var gotBody []byte ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotBody, _ = io.ReadAll(r.Body) _, _ = w.Write([]byte(`{"results":[{"url":"https://ex.com/a","title":"A","content":"s","score":0.2}]}`)) })) 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{IncludeDomainsMode: "prefer"}) if out.Warn != "" { t.Fatal(out.Warn) } var req map[string]any _ = json.Unmarshal(gotBody, &req) if _, ok := req["include_domains_mode"]; ok { t.Fatalf("mode without include: %+v", req) } if out.Hits[0].PublishedDate != "" { t.Fatal("missing date must omit") } } func TestSearxNGSiteAndParams(t *testing.T) { var gotQ, gotTR, gotCat string ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotQ = r.URL.Query().Get("q") gotTR = r.URL.Query().Get("time_range") gotCat = r.URL.Query().Get("categories") if strings.Contains(gotQ, "!news") || strings.Contains(gotQ, "!month") { t.Errorf("bang in q: %s", gotQ) } _, _ = w.Write([]byte(`{"results":[{"url":"https://a.example/","title":"a","content":"aa","score":1,"publishedDate":"Mon, 01 Sep 2026 00:00:00 GMT"}]}`)) })) defer ts.Close() c, err := dial.New(ts.URL, dial.Options{Loopback: true, Timeout: time.Second}) if err != nil { t.Fatal(err) } f := Filter{IncludeDomains: []string{"a.example"}, TimeRange: "month", Topic: "news", ExactMatch: true} out := (&SearxNG{Client: c}).Search(context.Background(), `"phrase" here`, 8, f) if !strings.HasPrefix(gotQ, "site:a.example ") { t.Fatalf("q %q", gotQ) } if !strings.Contains(gotQ, `"phrase" here`) || strings.Count(gotQ, `"phrase"`) != 1 { t.Fatalf("wrapped q %q", gotQ) } if gotTR != "month" || gotCat != "news" { t.Fatalf("params tr=%q cat=%q", gotTR, gotCat) } if out.Hits[0].PublishedDate == "" { t.Fatal("publishedDate") } } func TestBraveNotesAndUnchangedURL(t *testing.T) { var gotURL string ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotURL = r.URL.String() _, _ = w.Write([]byte(`{"sources":[{"url":"https://off.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) } f := Filter{TimeRange: "month", Topic: "news", ExactMatch: true, MinScore: 0.5, IncludeDomains: []string{"openai.com"}, IncludeDomainsMode: "prefer"} out := (&BraveLLM{Client: c, Key: "k"}).Search(context.Background(), `"phrase"`, 5, f) if !strings.Contains(gotURL, "q=") { t.Fatalf("url %s", gotURL) } if strings.Contains(gotURL, "time_range") || strings.Contains(gotURL, "site:") { t.Fatalf("brave grew params: %s", gotURL) } note := strings.Join(out.Notes, " ") for _, want := range []string{ "brave_llm: time_range not applied", "brave_llm: topic not applied", "brave_llm: exact_match not applied", "brave_llm: include_domains_mode=prefer not applied", "brave_llm: min_score not applied", } { if !strings.Contains(note, want) { t.Fatalf("missing %q in %v", want, out.Notes) } } }