Files
sift/internal/search/engine_test.go
T
kyle d7873403ff p3: search API, pinned sources, cache, CLI
POST /v1/search and /v2/search fan in SearXNG, Brave LLM Context, and
Tavily from config-fixed endpoints. Public sources refuse private A
records; SearXNG is a loopback-only client, not the fetch guard.

Cache keys include caller class. Restricted callers search like the
fleet; logs omit query, hosts, URLs, and upstream errors. Degraded
results are not stored. Tavily sends search_depth=basic and drops
answer. Missing keys surface in /healthz.

Default-on sources stay off until the bake-off. Query harvest is not
in this commit.
2026-09-23 12:17:51 -07:00

160 lines
4.7 KiB
Go

package search
import (
"context"
"path/filepath"
"strings"
"testing"
"time"
"git.wntrmute.dev/kyle/sift/internal/dcache"
)
type fakeSrc struct {
name string
hits []Hit
warn string
delay time.Duration
credits int
}
func (f fakeSrc) Name() string { return f.name }
func (f fakeSrc) Search(ctx context.Context, query string, k int) SourceOut {
if f.delay > 0 {
select {
case <-time.After(f.delay):
case <-ctx.Done():
return SourceOut{Warn: f.name + ": timeout"}
}
}
return SourceOut{Hits: f.hits, Warn: f.warn, Credits: f.credits}
}
type stubEmb struct {
ok bool
reason string
}
func (s stubEmb) Embed(ctx context.Context, inputs []string) ([][]float64, error) {
return nil, context.Canceled
}
func (s stubEmb) Acquire(ctx context.Context) (bool, string) { return s.ok, s.reason }
func (s stubEmb) Release() {}
func TestFanInPartial(t *testing.T) {
e := &Engine{
Sources: []Source{
fakeSrc{name: NameSearxNG, hits: []Hit{{URL: "https://ok.example/a", Title: "ok", Snippet: "x", Source: NameSearxNG}}},
fakeSrc{name: NameBraveLLM, warn: "brave_llm: 500"},
fakeSrc{name: NameTavily, delay: 2 * time.Second},
},
Timeout: 50 * time.Millisecond,
DefaultK: 8,
RerankOn: false,
}
r := e.Run(context.Background(), "q", 8, false, "titan", dcache.ClassFleet)
if len(r.Hits) != 1 || r.Hits[0].URL != "https://ok.example/a" {
t.Fatalf("hits %+v", r.Hits)
}
joined := strings.Join(r.Warnings, " ")
if !strings.Contains(joined, "500") || !strings.Contains(joined, "timeout") {
t.Fatalf("warnings %v", r.Warnings)
}
}
func TestCacheClassAndHour(t *testing.T) {
db, err := dcache.Open(filepath.Join(t.TempDir(), "c.db"), 1<<20, nil)
if err != nil {
t.Fatal(err)
}
defer db.Close()
now := time.Date(2026, 9, 23, 10, 0, 0, 0, time.UTC)
src := fakeSrc{name: NameSearxNG, hits: []Hit{{URL: "https://ok.example/a", Title: "ok", Snippet: "x", Source: NameSearxNG}}}
e := &Engine{Sources: []Source{src}, DefaultK: 8, Cache: db, RerankOn: false, Now: func() time.Time { return now }}
r1 := e.Run(context.Background(), "Hello World", 8, false, "titan", dcache.ClassFleet)
if r1.Cached {
t.Fatal("first miss")
}
r2 := e.Run(context.Background(), "hello world", 8, false, "titan", dcache.ClassFleet)
if !r2.Cached {
t.Fatal("norm/hour hit")
}
r3 := e.Run(context.Background(), "hello world", 8, false, "gs", dcache.ClassRestricted)
if r3.Cached {
t.Fatal("restricted must miss fleet cache")
}
e.Now = func() time.Time { return now.Add(time.Hour) }
r4 := e.Run(context.Background(), "hello world", 8, false, "titan", dcache.ClassFleet)
if r4.Cached {
t.Fatal("next hour must miss")
}
}
func TestNoCacheOnDegraded(t *testing.T) {
db, err := dcache.Open(filepath.Join(t.TempDir(), "c.db"), 1<<20, nil)
if err != nil {
t.Fatal(err)
}
defer db.Close()
e := &Engine{
Sources: []Source{
fakeSrc{name: NameSearxNG, hits: []Hit{{URL: "https://ok.example/a", Source: NameSearxNG}}},
fakeSrc{name: NameBraveLLM, warn: "brave_llm: 500"},
},
DefaultK: 8, Cache: db, RerankOn: false,
}
e.Run(context.Background(), "q", 8, false, "titan", dcache.ClassFleet)
r := e.Run(context.Background(), "q", 8, false, "titan", dcache.ClassFleet)
if r.Cached {
t.Fatal("degraded cached")
}
}
func TestRerankFallbackStill200(t *testing.T) {
e := &Engine{
Sources: []Source{fakeSrc{name: NameSearxNG, hits: []Hit{{URL: "https://ok.example/a", Source: NameSearxNG}}}},
DefaultK: 8,
RerankOn: true,
Embedder: stubEmb{ok: false, reason: "queue_full"},
}
r := e.Run(context.Background(), "q", 8, true, "titan", dcache.ClassFleet)
if len(r.Hits) != 1 {
t.Fatal(r.Hits)
}
if !strings.Contains(strings.Join(r.Warnings, " "), "rerank fell back") {
t.Fatalf("%v", r.Warnings)
}
}
func TestDailyCapSkipsTavily(t *testing.T) {
db, err := dcache.Open(filepath.Join(t.TempDir(), "c.db"), 1<<20, nil)
if err != nil {
t.Fatal(err)
}
defer db.Close()
var n int
tav := fakeSrc{name: NameTavily, hits: []Hit{{URL: "https://t.example/a", Source: NameTavily}}, credits: 1}
e := &Engine{Sources: []Source{tav}, DefaultK: 8, Cache: db, TavilyCap: 1, RerankOn: false}
e.Run(context.Background(), "q1", 8, false, "titan", dcache.ClassFleet)
r := e.Run(context.Background(), "q2", 8, false, "titan", dcache.ClassFleet)
for _, w := range r.Warnings {
if strings.Contains(w, "daily cap") {
n++
}
}
if n != 1 {
t.Fatalf("warnings %v", r.Warnings)
}
}
func TestDedupeLongerSnippet(t *testing.T) {
in := []Hit{
{URL: "https://Ex.com/a#x", Snippet: "aa", Source: NameSearxNG},
{URL: "https://ex.com/a", Snippet: "aabbcc", Source: NameTavily},
}
out := Dedupe(in)
if len(out) != 1 || out[0].Source != NameSearxNG || out[0].Snippet != "aabbcc" {
t.Fatalf("%+v", out)
}
}