package rag_test import ( "context" "net/http" "path/filepath" "testing" "time" llm_llamacpp "github.com/VictorVargas/rony-llm-agent/pkg/llm/providers/llamacpp" "github.com/VictorVargas/rony-llm-agent/pkg/rag" "github.com/VictorVargas/rony-llm-agent/pkg/rag/backends/sqlitevec" "github.com/VictorVargas/rony-llm-agent/pkg/rag/embeddings" ) // TestEpisodeCapture_EndToEndLocalServer exercises the full auto-capture // path — real LLM summarization, sqlitevec storage, taxonomy-filtered // recall — against the llama.cpp server Rony actually uses. Skipped when no // server is listening on localhost:8080, so it never breaks CI or offline // runs; with the server up it's the proof the feature works for real, not // just against stubs. func TestEpisodeCapture_EndToEndLocalServer(t *testing.T) { probe, err := (&http.Client{Timeout: 2 * time.Second}).Get("http://localhost:8080/v1/models") if err != nil { t.Skipf("no local llama.cpp server on :8080: %v", err) } probe.Body.Close() client, err := llm_llamacpp.New(llm_llamacpp.Config{}) if err != nil { t.Fatalf("llamacpp client: %v", err) } backend, err := sqlitevec.New(filepath.Join(t.TempDir(), "e2e_memory.db")) if err != nil { t.Fatalf("sqlitevec: %v", err) } defer backend.Close() // Same embedder wiring the harness uses: llama.cpp embeddings when the // server exposes them, transparent FTS5 fallback otherwise. embedder, err := embeddings.NewLlamaCpp(embeddings.LlamaCppConfig{}) if err != nil { t.Fatalf("embedder: %v", err) } mem, err := rag.New(rag.Config{Backend: backend, Embedder: embedder}) if err != nil { t.Fatalf("memory: %v", err) } capture := &rag.EpisodeCapture{ Memory: mem, LLM: client, ProjectID: "e2e-test", Prompt: "Resume el siguiente intercambio entre un usuario y un asistente de IA en 1-2 frases, en pasado, " + "enfocándote en qué se pidió y qué se hizo. Responde ÚNICAMENTE con el resumen. /no_think", } ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() userInput := "¿Puedes optimizar el cliente OpenAI del proyecto? El streaming no funciona." reply := "Encontré que Stream() enviaba stream:false y descartaba el modelo configurado. " + "Reescribí el cliente: ahora hace streaming real, acumula tool calls y reporta el usage. Los tests pasan." if err := capture.Capture(ctx, userInput, reply, "read", "edit", "bash"); err != nil { t.Fatalf("capture failed against the real server: %v", err) } // Recall it back, restricted to episodic memory. episodes, err := mem.SearchByType(ctx, "optimización del cliente OpenAI streaming", 5, rag.MemoryEpisodic) if err != nil { t.Fatalf("search failed: %v", err) } if len(episodes) == 0 { t.Fatal("expected the captured episode to be recallable via SearchByType(episodic)") } ep := episodes[0] if ep.Type != rag.MemoryEpisodic { t.Fatalf("expected episodic type, got %q", ep.Type) } if ep.Metadata["tools"] != "read,edit,bash" { t.Fatalf("expected tools metadata, got %q", ep.Metadata["tools"]) } if ep.Metadata["date"] == "" { t.Fatal("expected a date on the episode") } t.Logf("captured episode: %s", ep.Content) // A procedural-only search must NOT return the episode. procs, err := mem.SearchByType(ctx, "optimización del cliente OpenAI streaming", 5, rag.MemoryProcedural) if err != nil { t.Fatalf("procedural search failed: %v", err) } for _, p := range procs { if p.ID == ep.ID { t.Fatal("episode leaked into a procedural-only search") } } }