package rag
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"time"
"gitcode-mcp/internal/cache"
"gitcode-mcp/internal/config"
)
func TestFakeProviderDeterministicMultilingualEmbeddings(t *testing.T) {
provider, err := NewEmbeddingProviderFromConfig(fakeProviderConfig(), "", ProviderOptions{})
if err != nil {
t.Fatal(err)
}
fixture := loadMultilingualEvalFixture(t)
var inputs []string
for _, tc := range fixture.Cases {
inputs = append(inputs, tc.Query)
for _, chunk := range tc.Chunks {
inputs = append(inputs, chunk.Text)
}
}
first, err := provider.Embed(context.Background(), EmbedRequest{Inputs: inputs})
if err != nil {
t.Fatal(err)
}
second, err := provider.Embed(context.Background(), EmbedRequest{Inputs: inputs})
if err != nil {
t.Fatal(err)
}
if first.Dimensions != 8 || len(first.Embeddings) != len(inputs) {
t.Fatalf("first=%#v", first)
}
for i := range first.Embeddings {
for j := range first.Embeddings[i] {
if first.Embeddings[i][j] != second.Embeddings[i][j] {
t.Fatalf("embedding[%d][%d] not deterministic: %f != %f", i, j, first.Embeddings[i][j], second.Embeddings[i][j])
}
}
}
}
func TestEnsureEmbeddingNamespaceFromProviderProfile(t *testing.T) {
ctx := context.Background()
store, err := cache.NewSQLiteStore(ctx, ":memory:")
if err != nil {
t.Fatal(err)
}
defer store.Close()
if err := store.AddRepository(ctx, cache.RepositoryBinding{RepoID: "fixture-a", Owner: "owner", Name: "repo", APIBaseURL: "https://example.invalid/api", Scopes: []cache.RepositoryScope{cache.RepositoryScopeIssues}}); err != nil {
t.Fatal(err)
}
provider, err := NewEmbeddingProviderFromConfig(fakeProviderConfig(), "", ProviderOptions{})
if err != nil {
t.Fatal(err)
}
req := NamespaceRequest{RepoID: "fixture-a", ChunkPolicyID: "tokens-512-overlap-64", LanguagePolicyID: "ru-zh-en-v1"}
first, err := EnsureEmbeddingNamespace(ctx, store, provider, req)
if err != nil {
t.Fatal(err)
}
second, err := EnsureEmbeddingNamespace(ctx, store, provider, req)
if err != nil {
t.Fatal(err)
}
if first.ID == "" || second.ID != first.ID || first.ModelRevision != "fake-deterministic-v1" {
t.Fatalf("namespaces first=%#v second=%#v", first, second)
}
}
func TestEmbeddingProviderConfigValidation(t *testing.T) {
cfg := fakeProviderConfig()
profile := cfg.RAG.Profiles["fake"]
profile.Dimensions = 0
cfg.RAG.Profiles["fake"] = profile
if _, err := NewEmbeddingProviderFromConfig(cfg, "", ProviderOptions{}); err == nil || !strings.Contains(err.Error(), "dimensions") {
t.Fatalf("err=%v, want dimensions validation", err)
}
}
func TestOllamaProviderEmbedsAndClassifiesFailures(t *testing.T) {
t.Run("happy path", func(t *testing.T) {
server := newOllamaTestServer(t, ollamaServerBehavior{embedding: []float64{0.1, 0.2, 0.3}})
provider := newTestOllamaProvider(t, server.URL, 3)
result, err := provider.Embed(context.Background(), EmbedRequest{Inputs: []string{"русский 中文 English"}})
if err != nil {
t.Fatal(err)
}
if result.Model != "qwen3-embedding:0.6b" || result.Revision != "sha256:test" || result.Dimensions != 3 || len(result.Embeddings) != 1 {
t.Fatalf("result=%#v", result)
}
})
t.Run("missing model", func(t *testing.T) {
server := newOllamaTestServer(t, ollamaServerBehavior{models: []ollamaModel{{Name: "other", Digest: "sha256:other"}}})
provider := newTestOllamaProvider(t, server.URL, 3)
_, err := provider.Embed(context.Background(), EmbedRequest{Inputs: []string{"query"}})
assertProviderClass(t, err, ProviderFailureModelMissing)
})
t.Run("dimension mismatch", func(t *testing.T) {
server := newOllamaTestServer(t, ollamaServerBehavior{embedding: []float64{0.1, 0.2}})
provider := newTestOllamaProvider(t, server.URL, 3)
_, err := provider.Embed(context.Background(), EmbedRequest{Inputs: []string{"query"}})
assertProviderClass(t, err, ProviderFailureDimensionMismatch)
})
t.Run("unsupported response", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("{"))
}))
defer server.Close()
provider := newTestOllamaProvider(t, server.URL, 3)
_, err := provider.ModelInfo(context.Background())
assertProviderClass(t, err, ProviderFailureUnsupportedResponse)
})
t.Run("timeout", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(50 * time.Millisecond)
}))
defer server.Close()
provider := newTestOllamaProvider(t, server.URL, 3)
provider.httpClient.Timeout = time.Millisecond
_, err := provider.ModelInfo(context.Background())
assertProviderClass(t, err, ProviderFailureTimeout)
})
t.Run("retries transient unavailable", func(t *testing.T) {
var tagsCalls int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/tags" {
http.NotFound(w, r)
return
}
if atomic.AddInt32(&tagsCalls, 1) == 1 {
http.Error(w, "temporarily unavailable", http.StatusServiceUnavailable)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"models": []ollamaModel{{Name: "qwen3-embedding:0.6b", Digest: "sha256:test"}}})
}))
defer server.Close()
provider := newTestOllamaProvider(t, server.URL, 3)
info, err := provider.ModelInfo(context.Background())
if err != nil {
t.Fatal(err)
}
if info.Revision != "sha256:test" || atomic.LoadInt32(&tagsCalls) != 2 {
t.Fatalf("info=%#v tagsCalls=%d", info, atomic.LoadInt32(&tagsCalls))
}
})
}
func TestOllamaProviderUsesNativeBatchAndPreservesOrdering(t *testing.T) {
var embedCalls int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/tags":
_ = json.NewEncoder(w).Encode(map[string]any{"models": []ollamaModel{{Name: "qwen3-embedding:0.6b", Digest: "sha256:test"}}})
case "/api/embed":
atomic.AddInt32(&embedCalls, 1)
var body struct {
Model string `json:"model"`
Input []string `json:"input"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode native batch: %v", err)
}
if body.Model != "qwen3-embedding:0.6b" || len(body.Input) == 0 || len(body.Input) > 2 {
t.Fatalf("native batch body=%#v", body)
}
embeddings := make([][]float64, len(body.Input))
for i, input := range body.Input {
var value float64
if _, err := fmt.Sscanf(input, "input-%f", &value); err != nil {
t.Fatalf("parse input %q: %v", input, err)
}
embeddings[i] = []float64{value, 0, 0}
}
_ = json.NewEncoder(w).Encode(map[string]any{"embeddings": embeddings})
default:
http.NotFound(w, r)
}
}))
defer server.Close()
provider := newTestOllamaProvider(t, server.URL, 3)
inputs := []string{"input-1", "input-2", "input-3", "input-4", "input-5"}
result, err := provider.Embed(context.Background(), EmbedRequest{Inputs: inputs})
if err != nil {
t.Fatal(err)
}
if atomic.LoadInt32(&embedCalls) != 3 || len(result.Embeddings) != len(inputs) {
t.Fatalf("calls=%d result=%#v", embedCalls, result)
}
for i, embedding := range result.Embeddings {
if embedding[0] != float32(i+1) {
t.Fatalf("embedding[%d]=%v, want ordered value %d", i, embedding, i+1)
}
}
}
func TestOllamaProviderRejectsMalformedNativeBatch(t *testing.T) {
tests := []struct {
name string
response string
wantClass string
inputCount int
}{
{name: "partial cardinality", response: `{"embeddings":[[1,0,0]]}`, wantClass: ProviderFailureUnsupportedResponse, inputCount: 2},
{name: "malformed json", response: `{"embeddings":`, wantClass: ProviderFailureUnsupportedResponse, inputCount: 2},
{name: "dimension mismatch", response: `{"embeddings":[[1,0],[2,0,0]]}`, wantClass: ProviderFailureDimensionMismatch, inputCount: 2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/tags" {
_ = json.NewEncoder(w).Encode(map[string]any{"models": []ollamaModel{{Name: "qwen3-embedding:0.6b", Digest: "sha256:test"}}})
return
}
if r.URL.Path != "/api/embed" {
http.NotFound(w, r)
return
}
_, _ = w.Write([]byte(tt.response))
}))
defer server.Close()
provider := newTestOllamaProvider(t, server.URL, 3)
inputs := make([]string, tt.inputCount)
for i := range inputs {
inputs[i] = fmt.Sprintf("input-%d", i)
}
_, err := provider.Embed(context.Background(), EmbedRequest{Inputs: inputs})
assertProviderClass(t, err, tt.wantClass)
})
}
}
func TestOllamaProviderRetriesNativeBatchWithoutLegacyFallback(t *testing.T) {
var nativeCalls int32
var legacyCalls int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/tags":
_ = json.NewEncoder(w).Encode(map[string]any{"models": []ollamaModel{{Name: "qwen3-embedding:0.6b", Digest: "sha256:test"}}})
case "/api/embed":
if atomic.AddInt32(&nativeCalls, 1) == 1 {
http.Error(w, "temporarily unavailable", http.StatusServiceUnavailable)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"embeddings": [][]float64{{1, 0, 0}, {2, 0, 0}}})
case "/api/embeddings":
atomic.AddInt32(&legacyCalls, 1)
http.Error(w, "legacy should not be called", http.StatusInternalServerError)
default:
http.NotFound(w, r)
}
}))
defer server.Close()
provider := newTestOllamaProvider(t, server.URL, 3)
result, err := provider.Embed(context.Background(), EmbedRequest{Inputs: []string{"first", "second"}})
if err != nil {
t.Fatal(err)
}
if len(result.Embeddings) != 2 || nativeCalls != 2 || legacyCalls != 0 {
t.Fatalf("result=%#v native=%d legacy=%d", result, nativeCalls, legacyCalls)
}
}
func TestOllamaProviderFallsBackToLegacyOnceAndPreservesOrdering(t *testing.T) {
var nativeCalls int32
var legacyCalls int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/tags":
_ = json.NewEncoder(w).Encode(map[string]any{"models": []ollamaModel{{Name: "qwen3-embedding:0.6b", Digest: "sha256:test"}}})
case "/api/embed":
atomic.AddInt32(&nativeCalls, 1)
http.NotFound(w, r)
case "/api/embeddings":
atomic.AddInt32(&legacyCalls, 1)
var body struct {
Prompt string `json:"prompt"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode legacy request: %v", err)
}
value := float64(len(body.Prompt))
_ = json.NewEncoder(w).Encode(map[string]any{"embedding": []float64{value, 0, 0}})
default:
http.NotFound(w, r)
}
}))
defer server.Close()
provider := newTestOllamaProvider(t, server.URL, 3)
for _, inputs := range [][]string{{"a", "bbbb"}, {"cc", "dddddd"}} {
result, err := provider.Embed(context.Background(), EmbedRequest{Inputs: inputs})
if err != nil {
t.Fatal(err)
}
if result.Embeddings[0][0] != float32(len(inputs[0])) || result.Embeddings[1][0] != float32(len(inputs[1])) {
t.Fatalf("inputs=%v result=%#v", inputs, result)
}
}
if nativeCalls != 1 || legacyCalls != 4 {
t.Fatalf("native=%d legacy=%d, want 1 and 4", nativeCalls, legacyCalls)
}
}
func fakeProviderConfig() config.Config {
cfg := config.Default()
cfg.RAG.DefaultProfile = "fake"
cfg.RAG.Providers["fake"] = config.RAGProviderConfig{Type: "fake", Timeout: time.Second}
cfg.RAG.Profiles["fake"] = config.RAGProfileConfig{Provider: "fake", Model: "fake-embed", Dimensions: 8, BatchSize: 2, MaxInputTokens: 512}
cfg.RAG.Indexing.Profile = "fake"
cfg.RAG.Search.Profile = "fake"
return cfg
}
type ollamaServerBehavior struct {
models []ollamaModel
embedding []float64
}
func newOllamaTestServer(t *testing.T, behavior ollamaServerBehavior) *httptest.Server {
t.Helper()
if behavior.models == nil {
behavior.models = []ollamaModel{{Name: "qwen3-embedding:0.6b", Digest: "sha256:test"}}
}
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/tags":
_ = json.NewEncoder(w).Encode(map[string]any{"models": behavior.models})
case "/api/embed":
if behavior.embedding == nil {
behavior.embedding = []float64{0.1, 0.2, 0.3}
}
var body struct {
Input []string `json:"input"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode batch request: %v", err)
}
embeddings := make([][]float64, len(body.Input))
for i := range embeddings {
embeddings[i] = behavior.embedding
}
_ = json.NewEncoder(w).Encode(map[string]any{"embeddings": embeddings})
default:
http.NotFound(w, r)
}
}))
}
func newTestOllamaProvider(t *testing.T, endpoint string, dimensions int) *OllamaProvider {
t.Helper()
provider, err := NewOllamaProvider(EmbeddingProviderProfile{
ProfileID: "default",
ProviderID: "ollama",
ProviderType: "ollama",
Endpoint: endpoint,
Model: "qwen3-embedding:0.6b",
Dimensions: dimensions,
BatchSize: 2,
Timeout: time.Second,
}, ProviderOptions{MaxRetries: 0})
if err != nil {
t.Fatal(err)
}
return provider
}
func assertProviderClass(t *testing.T, err error, class string) {
t.Helper()
var providerErr *ProviderError
if !errors.As(err, &providerErr) {
t.Fatalf("err=%T %v, want ProviderError", err, err)
}
if providerErr.Class != class {
t.Fatalf("class=%q, want %q; err=%v", providerErr.Class, class, err)
}
if strings.TrimSpace(providerErr.Error()) == "" {
t.Fatalf("empty provider error")
}
}