package knowledge_test import ( "context" "encoding/json" "math" "net/http" "net/http/httptest" "strings" "testing" "github.com/krow/krow-backend/go-api/internal/knowledge" ) // The local embedder. // // Driven against a stub rather than a real Ollama, because what is being tested // is this package's half of the contract: the request shape, the normalisation, // and — most of all — what an operator is told when it does not work. The model // itself is somebody else's code and testing it here would test the network. func TestTheLocalEmbedderSendsWhatOllamaExpects(t *testing.T) { var got map[string]any srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/embed" { t.Errorf("posted to %s, want /api/embed", r.URL.Path) } json.NewDecoder(r.Body).Decode(&got) json.NewEncoder(w).Encode(map[string]any{ "embeddings": [][]float32{{3, 4}, {1, 0}}, }) })) defer srv.Close() e := knowledge.NewOllama(srv.URL, "nomic-embed-text", 2) out, err := e.Embed(context.Background(), []string{"a", "b"}, knowledge.KindDocument) if err != nil { t.Fatalf("embed: %v", err) } if got["model"] != "nomic-embed-text" { t.Errorf("model = %v", got["model"]) } if inputs, ok := got["input"].([]any); !ok || len(inputs) != 2 { t.Errorf("input = %v; the batch should travel as a list", got["input"]) } if len(out) != 2 { t.Fatalf("%d vectors, want 2", len(out)) } } func TestTheLocalEmbedderNormalisesWhatItGetsBack(t *testing.T) { // The schema's similarity function is a plain dot product, which equals // cosine similarity ONLY for unit vectors. Ollama returns whatever the // model produced. Skipping this would not error — it would rank badly, // dominated by whichever chunks happened to have the largest magnitude, // which is the kind of wrong that never looks broken. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { json.NewEncoder(w).Encode(map[string]any{"embeddings": [][]float32{{3, 4}}}) })) defer srv.Close() out, err := knowledge.NewOllama(srv.URL, "m", 2). Embed(context.Background(), []string{"x"}, knowledge.KindDocument) if err != nil { t.Fatalf("embed: %v", err) } var sum float64 for _, v := range out[0] { sum += float64(v) * float64(v) } if math.Abs(math.Sqrt(sum)-1) > 1e-5 { t.Errorf("vector has length %.4f, want 1 — the dot product will not be cosine similarity", math.Sqrt(sum)) } } func TestOllamaNotRunningSaysWhatToDo(t *testing.T) { // The single most likely failure, and the one where a bad message costs the // most time: an operator reading "connection refused" goes looking for a // network problem. e := knowledge.NewOllama("http://127.0.0.1:1", "nomic-embed-text", 768) _, err := e.Embed(context.Background(), []string{"x"}, knowledge.KindQuery) if err == nil { t.Fatal("embedding against nothing succeeded") } msg := err.Error() if !strings.Contains(msg, "ollama pull") { t.Errorf("the failure does not say how to fix it: %s", msg) } var kErr *knowledge.Error if !asErr(err, &kErr) || kErr.Code != knowledge.ErrEmbedUnavailable { t.Errorf("want %s, got %v", knowledge.ErrEmbedUnavailable, err) } // Retryable: a model that is starting up will answer in a moment. if !kErr.Retryable() { t.Error("an unreachable local model should be retryable") } } func TestAMissingModelIsADifferentProblemFromADeadServer(t *testing.T) { // Ollama up but never told to pull the model. Same symptom to a user, a // completely different fix — and one of them is not worth retrying. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) })) defer srv.Close() _, err := knowledge.NewOllama(srv.URL, "nomic-embed-text", 768). Embed(context.Background(), []string{"x"}, knowledge.KindQuery) var kErr *knowledge.Error if !asErr(err, &kErr) { t.Fatalf("want a knowledge error, got %v", err) } if kErr.Code != knowledge.ErrNotConfigured { t.Errorf("a missing model reported %s; it is a configuration problem, not an outage", kErr.Code) } if kErr.Retryable() { t.Error("a model that was never pulled will not appear by retrying") } if !strings.Contains(kErr.Message, "ollama pull") { t.Errorf("the failure does not name the fix: %s", kErr.Message) } } func TestALocalCorpusIsNotConfusedWithAHostedOne(t *testing.T) { // Vectors from two models are not comparable, and the model name on the // chunk row is the only thing standing between that and confident nonsense. // A local `nomic-embed-text` and a hosted model of the same name must not // share an identity. local := knowledge.NewOllama("", "nomic-embed-text", 768) if !strings.HasPrefix(local.Model(), "ollama/") { t.Errorf("local model name is %q; it must be distinguishable from a hosted one", local.Model()) } if local.Model() == knowledge.NewVoyage("k", "nomic-embed-text", 768).Model() { t.Error("a local and a hosted model with the same name share an identity") } }