Files
jbergner 173afb9c54
release-tag / release-image (push) Successful in 11m6s
1.5.2
2026-08-27 09:34:33 +02:00

220 lines
7.7 KiB
Go

package brain
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"neuroforge/internal/core"
)
func TestResearchLearnsSearXNGSnippetWithoutExplicitLearnPermission(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/embed" {
_ = json.NewEncoder(w).Encode(map[string]any{"embeddings": [][]float32{{1, 0, 0}}, "prompt_eval_count": 1})
return
}
http.NotFound(w, r)
}))
defer ollama.Close()
searx := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{"results": []map[string]any{{"title": "NVIDIA source", "url": "https://example.com/nvidia", "content": "NVIDIA develops GPUs and CUDA software.", "engine": "test"}}})
}))
defer searx.Close()
s, e := policyTestEngine(t, func(w http.ResponseWriter, r *http.Request) {
// policyTestEngine owns its own Ollama server, so forward the expected embed response here.
if r.URL.Path == "/api/embed" {
_ = json.NewEncoder(w).Encode(map[string]any{"embeddings": [][]float32{{1, 0, 0}}, "prompt_eval_count": 1})
return
}
http.NotFound(w, r)
})
_ = ollama // keep this test self-contained if provider setup changes later.
cfg := s.Config()
cfg.Research.Enabled = true
cfg.Research.SearXNG.Enabled = true
cfg.Research.SearXNG.BaseURL = searx.URL
cfg.Research.WebFetch.Enabled = false
cfg.Brain.LearningPolicy.AllowExplicitLearn = false
if err := s.UpdateConfig(cfg); err != nil {
t.Fatal(err)
}
out, err := e.Research(context.Background(), ResearchRequest{Query: "NVIDIA", Learn: true, FetchPages: false, MaxResults: 2})
if err != nil {
t.Fatal(err)
}
if out.Ingested != 1 || len(out.Sources) != 1 {
t.Fatalf("unexpected research result %#v", out)
}
if got := s.SourcesSnapshot(10); len(got) != 1 || got[0].Type != "search" {
t.Fatalf("unexpected sources %#v", got)
}
}
func TestGoalResearchFallsBackToGeneralWhenCategoryMixIsOffTopic(t *testing.T) {
var categories []string
searx := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cat := r.URL.Query().Get("categories")
categories = append(categories, cat)
if cat == "general" {
_ = json.NewEncoder(w).Encode(map[string]any{"results": []map[string]any{{
"title": "FortiClient SSL VPN error -7200 troubleshooting", "url": "https://community.fortinet.com/forticlient-7200", "content": "FortiClient SSLVPN error 7200 can indicate credential or SSL VPN configuration problems.", "engine": "general-test",
}}})
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"results": []map[string]any{{
"title": "Unrelated arXiv graph paper", "url": "https://arxiv.org/abs/1234.5678", "content": "Graph transformation research unrelated to VPN clients.", "engine": "arxiv",
}}})
}))
defer searx.Close()
s, e := policyTestEngine(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/embed" {
_ = json.NewEncoder(w).Encode(map[string]any{"embeddings": [][]float32{{1, 0, 0, 0}}, "prompt_eval_count": 1})
return
}
http.NotFound(w, r)
})
cfg := s.Config()
cfg.Research.Enabled = true
cfg.Research.SearXNG.Enabled = true
cfg.Research.SearXNG.BaseURL = searx.URL
cfg.Research.SearXNG.Categories = "general,science,it"
cfg.Research.WebFetch.Enabled = false
if err := s.UpdateConfig(cfg); err != nil {
t.Fatal(err)
}
goal := core.Goal{Title: "FortiClient SSLVPN 7200", Status: core.GoalActive, ResearchEnabled: true}
if err := s.UpsertGoal(&goal); err != nil {
t.Fatal(err)
}
out, err := e.Research(context.Background(), ResearchRequest{Query: "FortiClient SSLVPN 7200", Learn: true, FetchPages: false, MaxResults: 6, goalID: goal.ID})
if err != nil {
t.Fatal(err)
}
if len(categories) != 2 || categories[0] != "general,science,it" || categories[1] != "general" {
t.Fatalf("expected category fallback, calls=%#v", categories)
}
if len(out.Results) != 1 || out.Results[0].Engine != "general-test" || out.Ingested < 1 {
t.Fatalf("fallback result was not used: %#v", out)
}
}
func TestGoalResearchPersistsTransparentTrace(t *testing.T) {
searx := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{"results": []map[string]any{{
"title": "NVIDIA official evidence", "url": "https://example.com/nvidia", "content": "NVIDIA develops GPUs and the CUDA parallel computing platform.", "engine": "test", "score": 0.9,
}}})
}))
defer searx.Close()
s, e := policyTestEngine(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/embed" {
_ = json.NewEncoder(w).Encode(map[string]any{"embeddings": [][]float32{{1, 0, 0, 0}}, "prompt_eval_count": 1})
return
}
http.NotFound(w, r)
})
cfg := s.Config()
cfg.Research.Enabled = true
cfg.Research.SearXNG.Enabled = true
cfg.Research.SearXNG.BaseURL = searx.URL
cfg.Research.Goal.Enabled = true
cfg.Research.WebFetch.Enabled = false
cfg.Autonomy.UseLLM = false
cfg.Brain.ExternalRelinkWorker = false
if err := s.UpdateConfig(cfg); err != nil {
t.Fatal(err)
}
goal := core.Goal{Title: "NVIDIA", Description: "learn sourced NVIDIA facts", Status: core.GoalActive, Priority: 70, ResearchEnabled: true}
if err := s.UpsertGoal(&goal); err != nil {
t.Fatal(err)
}
cycle, err := e.RunGoalCycle(context.Background(), goal.ID)
if err != nil {
t.Fatal(err)
}
if cycle.ResearchRunID == "" {
t.Fatal("learning cycle has no research_run_id")
}
run, ok := s.LatestResearchRun(goal.ID)
if !ok {
t.Fatal("research run not persisted")
}
if run.ID != cycle.ResearchRunID || run.Status != "completed" {
t.Fatalf("unexpected run %#v", run)
}
if run.Stats.Results < 1 || run.Stats.Claims < 1 || run.Stats.NewEvidence < 1 {
t.Fatalf("trace stats do not expose search/claim/learning: %#v", run.Stats)
}
seen := map[string]bool{}
for _, ev := range run.Events {
seen[ev.Type] = true
}
for _, typ := range []string{"query.planned", "search.result", "claim.extracted", "evidence.learned", "run.finished"} {
if !seen[typ] {
t.Fatalf("missing research trace event %q; seen=%v", typ, seen)
}
}
}
func TestGoalCycleSingleFlightRejectsConcurrentRun(t *testing.T) {
started := make(chan struct{}, 1)
release := make(chan struct{})
searx := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case started <- struct{}{}:
default:
}
<-release
_ = json.NewEncoder(w).Encode(map[string]any{"results": []map[string]any{}})
}))
defer searx.Close()
s, e := policyTestEngine(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/embed" {
_ = json.NewEncoder(w).Encode(map[string]any{"embeddings": [][]float32{{1, 0, 0, 0}}})
return
}
http.NotFound(w, r)
})
cfg := s.Config()
cfg.Research.Enabled = true
cfg.Research.SearXNG.Enabled = true
cfg.Research.SearXNG.BaseURL = searx.URL
cfg.Research.Goal.Enabled = true
cfg.Research.WebFetch.Enabled = false
cfg.Brain.LearningPolicy.LearnGoalCycles = false
if err := s.UpdateConfig(cfg); err != nil {
t.Fatal(err)
}
goal := core.Goal{Title: "single flight", Description: "concurrency guard", Status: core.GoalActive, Priority: 70, ResearchEnabled: true}
if err := s.UpsertGoal(&goal); err != nil {
t.Fatal(err)
}
firstDone := make(chan error, 1)
go func() {
_, err := e.RunGoalCycle(context.Background(), goal.ID)
firstDone <- err
}()
select {
case <-started:
case <-time.After(2 * time.Second):
t.Fatal("first goal cycle did not enter research")
}
if _, err := e.RunGoalCycle(context.Background(), goal.ID); !errors.Is(err, ErrGoalCycleInProgress) {
t.Fatalf("second cycle error=%v, want %v", err, ErrGoalCycleInProgress)
}
close(release)
if err := <-firstDone; err != nil {
t.Fatalf("first cycle failed: %v", err)
}
}