ddb658ffbb
Step one of letting Maven read instead of recall. No LLM yet. internal/kiwix/client.go: search a local Kiwix server, parse the RSS reply, hand back title + path + plain-text snippet + word count. The snippet is the unit of context; a full article is ~100KB of HTML and will not fit a 4096 token window. internal/kiwix/retrieval_eval.go plus knowledge_v1.json: the 9 knowledge questions from the phrasing fixture, each with hand-written English keywords, scored on whether a wanted article comes back in the top 5. Opt-in via MAVEN_KIWIX_URL, since CI has no Kiwix. No pass bar, the number is the finding. Result on the live mirror: 8/8 answerable questions hit, 7 of them at rank 1. Retrieval works. Keywords are written by hand on purpose, since Kiwix ranks by keyword and not by meaning, so a natural question fails. A query-rewrite step is the next piece of work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
91 lines
2.9 KiB
Go
91 lines
2.9 KiB
Go
package kiwix
|
||
|
||
import (
|
||
"context"
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// A real reply from the live server, trimmed to two items.
|
||
const sampleRSS = `<?xml version="1.0" encoding="UTF-8"?>
|
||
<rss version="2.0" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">
|
||
<channel>
|
||
<title>Search: Rayleigh scattering</title>
|
||
<opensearch:totalResults>800</opensearch:totalResults>
|
||
<item>
|
||
<title>Rayleigh scattering</title>
|
||
<link>/content/wikipedia_en_all_maxi_2026-02/Rayleigh_scattering</link>
|
||
<description><b>Rayleigh</b> scattering causes the blue color of the sky & yellow colors near the Sun.[1]</description>
|
||
<book><title>Wikipedia</title></book>
|
||
<wordCount>2,818</wordCount>
|
||
</item>
|
||
<item>
|
||
<title>Hyper–Rayleigh scattering</title>
|
||
<link>/content/wikipedia_en_all_maxi_2026-02/Hyper%E2%80%93Rayleigh_scattering</link>
|
||
<description>...<b>Rayleigh</b> scattering" is a nonlinear optical counterpart.</description>
|
||
<book><title>Wikipedia</title></book>
|
||
<wordCount>914</wordCount>
|
||
</item>
|
||
</channel>
|
||
</rss>`
|
||
|
||
func TestParseSearchRSS(t *testing.T) {
|
||
got, err := ParseSearchRSS(strings.NewReader(sampleRSS))
|
||
if err != nil {
|
||
t.Fatalf("parse: %v", err)
|
||
}
|
||
if len(got) != 2 {
|
||
t.Fatalf("want 2 results, got %d", len(got))
|
||
}
|
||
if got[0].Title != "Rayleigh scattering" {
|
||
t.Errorf("title = %q", got[0].Title)
|
||
}
|
||
if got[0].Path != "/content/wikipedia_en_all_maxi_2026-02/Rayleigh_scattering" {
|
||
t.Errorf("path = %q", got[0].Path)
|
||
}
|
||
if got[0].WordCount != 2818 {
|
||
t.Errorf("wordCount = %d", got[0].WordCount)
|
||
}
|
||
want := "Rayleigh scattering causes the blue color of the sky & yellow colors near the Sun.[1]"
|
||
if got[0].Snippet != want {
|
||
t.Errorf("snippet = %q, want %q", got[0].Snippet, want)
|
||
}
|
||
if strings.Contains(got[1].Snippet, "<b>") {
|
||
t.Errorf("second snippet still has tags: %q", got[1].Snippet)
|
||
}
|
||
}
|
||
|
||
func TestParseSearchRSSBadXML(t *testing.T) {
|
||
if _, err := ParseSearchRSS(strings.NewReader("not xml at all")); err == nil {
|
||
t.Fatal("want an error on junk input")
|
||
}
|
||
}
|
||
|
||
// Opt-in: needs a live Kiwix server. CI has none.
|
||
// MAVEN_KIWIX_URL=http://127.0.0.1:8034 no_proxy=127.0.0.1,localhost go test -run Retrieval -v ./internal/kiwix/
|
||
func TestRetrievalEval(t *testing.T) {
|
||
base := os.Getenv("MAVEN_KIWIX_URL")
|
||
if base == "" {
|
||
t.Skip("set MAVEN_KIWIX_URL to run the retrieval eval")
|
||
}
|
||
noProxyLoopback(t)
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||
defer cancel()
|
||
|
||
rep, err := RunRetrievalEval(ctx, New(base), 5)
|
||
if err != nil {
|
||
t.Fatalf("eval: %v", err)
|
||
}
|
||
// No pass bar on purpose: the number is the finding.
|
||
t.Log("\n" + rep.String() + rep.Detail())
|
||
}
|
||
|
||
// noProxyLoopback stops the box's SOCKS bridge from eating loopback requests.
|
||
func noProxyLoopback(t *testing.T) {
|
||
t.Setenv("no_proxy", "127.0.0.1,localhost")
|
||
t.Setenv("NO_PROXY", "127.0.0.1,localhost")
|
||
}
|