From e90f281f6bdc32c923e6b03b734b04df62993f3f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 00:33:03 +0000 Subject: [PATCH] test: add Phase 3 integration tests with live Qdrant + Ollama (69.0% -> 89.2%) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 32 new integration tests across 3 files, all gated behind //go:build rag: - qdrant_integration_test.go (11): collection CRUD, upsert, search, filter, overwrite - ollama_integration_test.go (9): embed, batch, consistency, dimension, model verify - integration_test.go (12): end-to-end ingest+query, format results, all helpers, semantic similarity, recreate flag, convenience wrappers with default clients Key discovery: Qdrant NewID() requires valid UUID/hex format — arbitrary strings rejected. ChunkID's MD5 hex output works, but test point IDs must match. Co-Authored-By: Charon --- FINDINGS.md | 53 +++++ TODO.md | 8 +- integration_test.go | 428 +++++++++++++++++++++++++++++++++++++ ollama_integration_test.go | 142 ++++++++++++ qdrant_integration_test.go | 314 +++++++++++++++++++++++++++ 5 files changed, 941 insertions(+), 4 deletions(-) create mode 100644 integration_test.go create mode 100644 ollama_integration_test.go create mode 100644 qdrant_integration_test.go diff --git a/FINDINGS.md b/FINDINGS.md index 3847b10..5f29ac6 100644 --- a/FINDINGS.md +++ b/FINDINGS.md @@ -193,3 +193,56 @@ Added interface-accepting helpers that the convenience wrappers now delegate to: |------|---------| | embedder.go | `Embedder` interface definition | | vectorstore.go | `VectorStore` interface definition | + +--- + +## 2026-02-20: Phase 3 Integration Tests with Live Services (go-rag agent) + +### Coverage Improvement + +``` +Before: 69.0% (135 tests across 7 test files, mock-based only) +After: 89.2% (204 tests across 10 test files, includes live Qdrant + Ollama) +``` + +### Infrastructure Verified + +| Service | Version | Status | Connection | +|---------|---------|--------|------------| +| Qdrant | 1.16.3 | Running (Docker) | gRPC localhost:6334, REST localhost:6333 | +| Ollama | native + ROCm | Running | HTTP localhost:11434, model: nomic-embed-text (F16, 274MB) | + +### Discoveries + +1. **Qdrant point IDs must be valid UUIDs** -- `qdrant.NewID()` wraps the string as a UUID field. Qdrant's server-side UUID parser accepts 32-character hex strings (as produced by `ChunkID` via MD5) but rejects arbitrary strings like `point-alpha`. Error: `Unable to parse UUID: point-alpha`. Integration tests must use `ChunkID()` or MD5 hex format for point IDs. + +2. **Qdrant Go client version warning is benign** -- The client library (v1.16.2) logs `WARN Unable to compare versions` and `Client version is not compatible with server version` when connecting to Qdrant v1.16.3. This is a cosmetic mismatch in version parsing — all operations function correctly despite the warning. + +3. **Qdrant indexing latency** -- After upserting points, a 500ms sleep is needed before searching to avoid flaky results. For small datasets the indexing is nearly instant, but the sleep provides a safety margin on slower machines. + +4. **Ollama embedding determinism** -- Embedding the same text twice with `nomic-embed-text` produces bit-identical vectors (`float32` level). This is important for idempotent ingest operations. + +5. **Ollama accepts empty strings** -- `Embed(ctx, "")` returns a valid 768-dimension vector without error. This is Ollama-specific behaviour and may differ with other embedding providers. + +6. **Semantic similarity works as expected** -- When ingesting both programming and cooking documents, a query about "Go functions and closures" correctly ranks the programming document highest. The cosine distance metric in Qdrant combined with nomic-embed-text embeddings provides meaningful semantic differentiation. + +7. **Convenience wrappers (QueryDocs, IngestDirectory) create their own gRPC connections** -- Each call to `QueryDocs` or `IngestDirectory` establishes a new Qdrant gRPC connection. In production this is fine for CLI commands, but for high-throughput scenarios the `*With` variants that accept pre-created clients should be preferred. + +8. **Remaining ~11% untested** -- The uncovered code is primarily error-handling branches in `NewQdrantClient` (connection failure), `Close()`, and the `filepath.Rel` error branch in `Ingest`. These represent defensive code paths that are difficult to trigger in normal operation. + +### Test Files Created + +| File | Tests | What It Covers | +|------|-------|----------------| +| qdrant_integration_test.go | 11 | Health check, create/delete/list/info collection, exists check, upsert+search, filter, empty upsert, ID validation, overwrite | +| ollama_integration_test.go | 9 | Verify model, embed single, embed batch, consistency, dimension match, model name, different texts, non-zero values, empty string | +| integration_test.go | 12 | End-to-end ingest+query, format results, IngestFile, QueryWith, QueryContextWith, IngestDirWith, IngestFileWith, QueryDocs, IngestDirectory, recreate flag, semantic similarity | + +### Build Tag Strategy + +All integration tests use `//go:build rag` to isolate them from CI runs that lack live services: + +```bash +go test ./... -count=1 # 135 tests, 69.0% — mock-only, no services needed +go test -tags rag ./... -count=1 # 204 tests, 89.2% — requires Qdrant + Ollama +``` diff --git a/TODO.md b/TODO.md index 67ebfad..67ac393 100644 --- a/TODO.md +++ b/TODO.md @@ -26,9 +26,9 @@ All pure-function tests complete. Remaining untested functions require live serv ### Require External Services (use build tag `//go:build rag`) -- [ ] **Qdrant client tests** — Create collection, upsert, search, delete. Skip if Qdrant unavailable. -- [ ] **Ollama client tests** — Embed single text, embed batch, verify model. Skip if Ollama unavailable. -- [ ] **Query integration test** — Ingest a test doc, query it, verify results. +- [x] **Qdrant client tests** — Create collection, upsert, search, delete, list, info, filter, overwrite. Skip if Qdrant unavailable. 11 subtests in `qdrant_integration_test.go`. (PHASE3_COMMIT) +- [x] **Ollama client tests** — Embed single text, embed batch, verify model, consistency, dimension check, different texts, non-zero values, empty string. 9 subtests in `ollama_integration_test.go`. (PHASE3_COMMIT) +- [x] **Full pipeline integration test** — Ingest directory, query, format results, all helpers (QueryWith, QueryContextWith, IngestDirWith, IngestFileWith, QueryDocs, IngestDirectory), recreate flag, semantic similarity. 12 subtests in `integration_test.go`. (PHASE3_COMMIT) ## Phase 2: Test Infrastructure (38.8% -> 69.0% coverage) @@ -55,7 +55,7 @@ All pure-function tests complete. Remaining untested functions require live serv ## Known Issues 1. **go.mod had wrong replace path** — `../core` should be `../go`. Fixed by Charon. -2. **Qdrant and Ollama not running on snider-linux** — Need docker setup for Qdrant, native install for Ollama. +2. ~~**Qdrant and Ollama not running on snider-linux**~~ — **Resolved.** Qdrant v1.16.3 (Docker) and Ollama with ROCm + nomic-embed-text now running on localhost. 3. ~~**No mocks/interfaces**~~ — **Resolved in Phase 2.** `Embedder` and `VectorStore` interfaces extracted; mock implementations in `mock_test.go`. 4. **`log.E` returns error** — `forge.lthn.ai/core/go/pkg/log.E` wraps errors with component context. This is the framework's logging pattern. diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..6d480b8 --- /dev/null +++ b/integration_test.go @@ -0,0 +1,428 @@ +//go:build rag + +package rag + +import ( + "context" + "fmt" + "net" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// skipIfServicesUnavailable skips the test if either Qdrant or Ollama is not +// reachable. Full pipeline tests need both. +func skipIfServicesUnavailable(t *testing.T) { + t.Helper() + for _, addr := range []string{"localhost:6334", "localhost:11434"} { + conn, err := net.DialTimeout("tcp", addr, 2*time.Second) + if err != nil { + t.Skipf("%s not available — skipping pipeline integration test", addr) + } + _ = conn.Close() + } +} + +func TestPipelineIntegration(t *testing.T) { + skipIfServicesUnavailable(t) + + ctx := context.Background() + + // Create shared clients for the pipeline tests. + qdrantCfg := DefaultQdrantConfig() + qdrantClient, err := NewQdrantClient(qdrantCfg) + require.NoError(t, err) + t.Cleanup(func() { _ = qdrantClient.Close() }) + + ollamaCfg := DefaultOllamaConfig() + ollamaClient, err := NewOllamaClient(ollamaCfg) + require.NoError(t, err) + + t.Run("ingest and query end-to-end", func(t *testing.T) { + collection := fmt.Sprintf("test-pipeline-%d", time.Now().UnixNano()) + t.Cleanup(func() { + _ = qdrantClient.DeleteCollection(ctx, collection) + }) + + // Create temp directory with markdown files + dir := t.TempDir() + writeTestFile(t, filepath.Join(dir, "go-intro.md"), `# Go Programming + +## Overview + +Go is an open-source programming language designed at Google. It features +garbage collection, structural typing, and CSP-style concurrency. Go was +created by Robert Griesemer, Rob Pike, and Ken Thompson. + +## Concurrency + +Go provides goroutines and channels for concurrent programming. Goroutines +are lightweight threads managed by the Go runtime. Channels allow goroutines +to communicate safely without shared memory. +`) + + writeTestFile(t, filepath.Join(dir, "qdrant-intro.md"), `# Qdrant Vector Database + +## What Is Qdrant + +Qdrant is a vector similarity search engine and vector database. It provides +a convenient API to store, search, and manage points with payload. Qdrant is +written in Rust and supports filtering, quantisation, and distributed deployment. + +## Use Cases + +Qdrant is commonly used for semantic search, recommendation systems, and +retrieval-augmented generation (RAG) pipelines. It supports cosine, dot product, +and Euclidean distance metrics. +`) + + writeTestFile(t, filepath.Join(dir, "rust-intro.md"), `# Rust Programming + +## Memory Safety + +Rust guarantees memory safety without a garbage collector through its ownership +system. The borrow checker enforces rules at compile time, preventing data races, +dangling pointers, and buffer overflows. +`) + + // Ingest the directory + ingestCfg := DefaultIngestConfig() + ingestCfg.Directory = dir + ingestCfg.Collection = collection + ingestCfg.Chunk = ChunkConfig{Size: 500, Overlap: 50} + + stats, err := Ingest(ctx, qdrantClient, ollamaClient, ingestCfg, nil) + require.NoError(t, err, "ingest should succeed") + assert.Equal(t, 3, stats.Files, "all three files should be ingested") + assert.Greater(t, stats.Chunks, 0, "should produce at least one chunk") + assert.Equal(t, 0, stats.Errors, "no errors should occur during ingest") + + // Allow Qdrant to index + time.Sleep(1 * time.Second) + + // Query for Go-related content + queryCfg := DefaultQueryConfig() + queryCfg.Collection = collection + queryCfg.Limit = 5 + queryCfg.Threshold = 0.0 // Accept all results for testing + + results, err := Query(ctx, qdrantClient, ollamaClient, "goroutines and channels in Go", queryCfg) + require.NoError(t, err, "query should succeed") + require.NotEmpty(t, results, "query should return at least one result") + + // The top result should be about Go concurrency + foundGoContent := false + for _, r := range results { + if r.Source != "" && r.Text != "" { + foundGoContent = true + break + } + } + assert.True(t, foundGoContent, "results should contain content with source and text fields") + + // Verify all results have expected metadata fields populated + for i, r := range results { + assert.NotEmpty(t, r.Text, "result %d should have text", i) + assert.NotEmpty(t, r.Source, "result %d should have source", i) + assert.NotEmpty(t, r.Category, "result %d should have category", i) + assert.Greater(t, r.Score, float32(0.0), "result %d should have positive score", i) + } + }) + + t.Run("format results from real query", func(t *testing.T) { + collection := fmt.Sprintf("test-format-%d", time.Now().UnixNano()) + t.Cleanup(func() { + _ = qdrantClient.DeleteCollection(ctx, collection) + }) + + dir := t.TempDir() + writeTestFile(t, filepath.Join(dir, "format-test.md"), `## Format Test + +This document is used to verify that the format functions produce non-empty +output when given real query results from live services. +`) + + ingestCfg := DefaultIngestConfig() + ingestCfg.Directory = dir + ingestCfg.Collection = collection + + _, err := Ingest(ctx, qdrantClient, ollamaClient, ingestCfg, nil) + require.NoError(t, err) + time.Sleep(1 * time.Second) + + queryCfg := DefaultQueryConfig() + queryCfg.Collection = collection + queryCfg.Limit = 3 + queryCfg.Threshold = 0.0 + + results, err := Query(ctx, qdrantClient, ollamaClient, "format test document", queryCfg) + require.NoError(t, err) + require.NotEmpty(t, results, "should return at least one result for formatting") + + // FormatResultsText + textOutput := FormatResultsText(results) + assert.NotEmpty(t, textOutput) + assert.NotEqual(t, "No results found.", textOutput) + assert.Contains(t, textOutput, "Result 1") + assert.Contains(t, textOutput, "Source:") + + // FormatResultsContext + ctxOutput := FormatResultsContext(results) + assert.NotEmpty(t, ctxOutput) + assert.Contains(t, ctxOutput, "") + assert.Contains(t, ctxOutput, "") + assert.Contains(t, ctxOutput, "