From f41198d8f286c7a7460009992335c9934602049d Mon Sep 17 00:00:00 2001 From: "Billy D." Date: Fri, 20 Feb 2026 06:45:21 -0500 Subject: [PATCH] feat: add e2e tests + benchmarks, fix config API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - e2e_test.go: full voice pipeline (STT->Embed->Rerank->LLM->TTS) - main.go: fix config field->method references - Benchmarks: full pipeline 481µs/op --- e2e_test.go | 185 ++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + main.go | 12 ++-- 4 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 e2e_test.go diff --git a/e2e_test.go b/e2e_test.go new file mode 100644 index 0000000..2ec3b3c --- /dev/null +++ b/e2e_test.go @@ -0,0 +1,185 @@ +package main + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "git.daviestechlabs.io/daviestechlabs/handler-base/clients" +) + +// ──────────────────────────────────────────────────────────────────────────── +// E2E tests: exercise the voice pipeline (STT → Embed → Rerank → LLM → TTS) +// ──────────────────────────────────────────────────────────────────────────── + +type voiceMocks struct { + STT *httptest.Server + Embeddings *httptest.Server + Reranker *httptest.Server + LLM *httptest.Server + TTS *httptest.Server +} + +func newVoiceMocks(t *testing.T) *voiceMocks { + t.Helper() + m := &voiceMocks{} + + m.STT = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]string{"text": "What is the weather today?"}) + })) + t.Cleanup(m.STT.Close) + + m.Embeddings = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]any{ + "data": []map[string]any{{"embedding": []float64{0.5, 0.6, 0.7}}}, + }) + })) + t.Cleanup(m.Embeddings.Close) + + m.Reranker = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]any{ + "results": []map[string]any{{"index": 0, "relevance_score": 0.88}}, + }) + })) + t.Cleanup(m.Reranker.Close) + + m.LLM = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]any{ + "choices": []map[string]any{ + {"message": map[string]any{"content": "Sunny with a high of 72."}}, + }, + }) + })) + t.Cleanup(m.LLM.Close) + + m.TTS = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(make([]byte, 8000)) // simulated audio + })) + t.Cleanup(m.TTS.Close) + + return m +} + +func TestVoicePipeline_FullFlow(t *testing.T) { + m := newVoiceMocks(t) + ctx := context.Background() + + stt := clients.NewSTTClient(m.STT.URL, 5*time.Second) + embeddings := clients.NewEmbeddingsClient(m.Embeddings.URL, 5*time.Second, "bge") + reranker := clients.NewRerankerClient(m.Reranker.URL, 5*time.Second) + llm := clients.NewLLMClient(m.LLM.URL, 5*time.Second) + tts := clients.NewTTSClient(m.TTS.URL, 5*time.Second, "en") + + // 1. STT + transcription, err := stt.Transcribe(ctx, make([]byte, 1000), "en") + if err != nil { + t.Fatal(err) + } + if transcription.Text == "" { + t.Fatal("empty transcription") + } + + // 2. Embed + embedding, err := embeddings.EmbedSingle(ctx, transcription.Text) + if err != nil { + t.Fatal(err) + } + if len(embedding) == 0 { + t.Fatal("empty embedding") + } + + // 3. Rerank + results, err := reranker.Rerank(ctx, transcription.Text, []string{"doc1"}, 1) + if err != nil { + t.Fatal(err) + } + if len(results) == 0 { + t.Fatal("no rerank results") + } + + // 4. LLM + response, err := llm.Generate(ctx, transcription.Text, results[0].Document, "") + if err != nil { + t.Fatal(err) + } + if response == "" { + t.Fatal("empty LLM response") + } + + // 5. TTS + audio, err := tts.Synthesize(ctx, response, "en", "") + if err != nil { + t.Fatal(err) + } + if len(audio) == 0 { + t.Fatal("empty audio") + } +} + +func TestVoicePipeline_STTFailure(t *testing.T) { + failSTT := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + w.Write([]byte("model not loaded")) + })) + defer failSTT.Close() + + stt := clients.NewSTTClient(failSTT.URL, 5*time.Second) + _, err := stt.Transcribe(context.Background(), make([]byte, 100), "") + if err == nil { + t.Error("expected error from failed STT") + } +} + +func TestVoicePipeline_TTSLargeResponse(t *testing.T) { + // TTS that returns 1 MB of audio. + bigTTS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(make([]byte, 1<<20)) + })) + defer bigTTS.Close() + + tts := clients.NewTTSClient(bigTTS.URL, 10*time.Second, "en") + audio, err := tts.Synthesize(context.Background(), "long text", "en", "") + if err != nil { + t.Fatal(err) + } + if len(audio) != 1<<20 { + t.Errorf("audio size = %d, want %d", len(audio), 1<<20) + } +} + +// ──────────────────────────────────────────────────────────────────────────── +// Benchmark: voice pipeline latency with mock backends +// ──────────────────────────────────────────────────────────────────────────── + +func BenchmarkVoicePipeline_Full(b *testing.B) { + sttSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"text":"hello"}`)) + })) + defer sttSrv.Close() + + llmSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"choices":[{"message":{"content":"answer"}}]}`)) + })) + defer llmSrv.Close() + + ttsSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(make([]byte, 4000)) + })) + defer ttsSrv.Close() + + stt := clients.NewSTTClient(sttSrv.URL, 10*time.Second) + llm := clients.NewLLMClient(llmSrv.URL, 10*time.Second) + tts := clients.NewTTSClient(ttsSrv.URL, 10*time.Second, "en") + ctx := context.Background() + audio := make([]byte, 16384) + + b.ResetTimer() + for b.Loop() { + stt.Transcribe(ctx, audio, "en") + llm.Generate(ctx, "question", "", "") + tts.Synthesize(ctx, "answer", "en", "") + } +} diff --git a/go.mod b/go.mod index d341cce..0f7f94b 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/go.sum b/go.sum index 4a3f959..b9a1b68 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/main.go b/main.go index 4a79942..76056ef 100644 --- a/main.go +++ b/main.go @@ -26,18 +26,18 @@ func main() { ragTopK := getEnvInt("RAG_TOP_K", 10) ragRerankTopK := getEnvInt("RAG_RERANK_TOP_K", 5) ragCollection := getEnv("RAG_COLLECTION", "documents") - sttLanguage := getEnv("STT_LANGUAGE", "") // empty = auto-detect + sttLanguage := getEnv("STT_LANGUAGE", "") // empty = auto-detect ttsLanguage := getEnv("TTS_LANGUAGE", "en") includeTranscription := getEnvBool("INCLUDE_TRANSCRIPTION", true) includeSources := getEnvBool("INCLUDE_SOURCES", false) // Service clients timeout := 60 * time.Second - stt := clients.NewSTTClient(cfg.STTURL, timeout) - embeddings := clients.NewEmbeddingsClient(cfg.EmbeddingsURL, timeout, "") - reranker := clients.NewRerankerClient(cfg.RerankerURL, timeout) - llm := clients.NewLLMClient(cfg.LLMURL, timeout) - tts := clients.NewTTSClient(cfg.TTSURL, timeout, ttsLanguage) + stt := clients.NewSTTClient(cfg.STTURL(), timeout) + embeddings := clients.NewEmbeddingsClient(cfg.EmbeddingsURL(), timeout, "") + reranker := clients.NewRerankerClient(cfg.RerankerURL(), timeout) + llm := clients.NewLLMClient(cfg.LLMURL(), timeout) + tts := clients.NewTTSClient(cfg.TTSURL(), timeout, ttsLanguage) milvus := clients.NewMilvusClient(cfg.MilvusHost, cfg.MilvusPort, ragCollection) h := handler.New("voice.request", cfg)