Files
Maven/cmd/mavend/actions_query_search_test.go
kami 14e98334ad query: let her search the live web before she reads the ZIMs
The offline encyclopedia was the only world source, and it reads what was true
when the ZIM was built. A self-hosted SearXNG now asks first and Kiwix is the
fallback for an empty result, an unreachable instance or no line out. Owner's
ruling, 2026-08-02.

internal/websearch is deliberately thin: no rewriter (SearXNG ranks through
real engines, so the Russian question goes out as he asked it), no page fetch,
no cache. It cannot read the store, so only the query string can leave the box.

The personal boundary is unchanged and still sits above this source, so a
question about him is never searched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaeSbLMEVG5ey8tejU3y2
2026-08-02 02:24:36 +04:00

106 lines
3.6 KiB
Go

package main
import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/kami/maven/internal/router"
"github.com/kami/maven/internal/websearch"
)
func searchHandler(t *testing.T, body string, status int) (*reactiveHandler, *string) {
t.Helper()
var seen string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seen = r.URL.RawQuery
if status != http.StatusOK {
http.Error(w, "no", status)
return
}
w.Write([]byte(body))
}))
t.Cleanup(srv.Close)
return &reactiveHandler{
// No phraser: querySearch then reads back the best evidence, which is
// what makes the claim visible without a llama-server in the test.
search: &searchWiring{client: websearch.New(srv.URL, websearch.Options{}), max: 3, runes: 1500},
}, &seen
}
const searchBody = `{"answers":["Небо голубое из-за рэлеевского рассеяния."],
"results":[{"title":"Рэлеевское рассеяние","url":"https://ru.wikipedia.org/x","content":"Рассеяние света."}]}`
func TestQuerySearchClaimsAndReadsBack(t *testing.T) {
h, _ := searchHandler(t, searchBody, http.StatusOK)
reply, ok := h.querySearch(context.Background(), &queryTurn{
dec: router.Decision{Intent: router.IntentQuery, Utterance: "почему небо голубое"},
})
if !ok {
t.Fatal("querySearch passed on a search with hits")
}
if !strings.Contains(reply, "рэлеевского рассеяния") {
t.Fatalf("reply = %q", reply)
}
}
// No rewriter in front of this source: SearXNG ranks by meaning, and reducing
// the question to English keywords would throw away the language he asked in.
func TestQuerySearchSendsTheQuestionVerbatim(t *testing.T) {
h, seen := searchHandler(t, searchBody, http.StatusOK)
h.querySearch(context.Background(), &queryTurn{
dec: router.Decision{Intent: router.IntentQuery, Utterance: "почему небо голубое"},
})
if !strings.Contains(*seen, "q="+url.QueryEscape("почему небо голубое")) {
t.Fatalf("query string = %q", *seen)
}
}
// The whole reason the ordering is safe: an unreachable or empty instance
// passes the turn to Kiwix instead of claiming it with an apology.
func TestQuerySearchFallsThroughWhenItFails(t *testing.T) {
for _, tc := range []struct {
name string
body string
status int
}{
{"http error", "", http.StatusForbidden},
{"no hits", `{"answers":[],"results":[]}`, http.StatusOK},
} {
t.Run(tc.name, func(t *testing.T) {
h, _ := searchHandler(t, tc.body, tc.status)
if _, ok := h.querySearch(context.Background(), &queryTurn{
dec: router.Decision{Intent: router.IntentQuery, Utterance: "почему небо голубое"},
}); ok {
t.Fatal("querySearch claimed the turn; Kiwix never got its fallback")
}
})
}
}
// Off unless configured, and silent about it: he never asked for a capability
// he did not enable.
func TestQuerySearchOffWithoutConfig(t *testing.T) {
h := &reactiveHandler{}
if _, ok := h.querySearch(context.Background(), &queryTurn{
dec: router.Decision{Intent: router.IntentQuery, Utterance: "почему небо голубое"},
}); ok {
t.Fatal("querySearch claimed a turn with no search block")
}
}
// The owner's ruling of 2026-08-02: the live search asks first, the ZIM is the
// fallback for a box with no line out.
func TestSearchRunsBeforeKiwix(t *testing.T) {
idx := map[string]int{}
for i, s := range querySources {
idx[s.name] = i
}
if idx["search"] > idx["kiwix"] {
t.Fatalf("search at %d, kiwix at %d: the ZIM is the fallback, not the first read", idx["search"], idx["kiwix"])
}
}