149 lines
7.2 KiB
Go
149 lines
7.2 KiB
Go
package graph
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/local/glpi-neural-brain/internal/activity"
|
|
"github.com/local/glpi-neural-brain/internal/model"
|
|
)
|
|
|
|
func TestMutationStatsAndAnalysisHistory(t *testing.T) {
|
|
store, err := Open(t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = store.Close() })
|
|
broker := activity.New(20)
|
|
broker.SetSink(store.RecordActivity)
|
|
|
|
store.UpsertNode(model.Node{ID: "a", Kind: "knowledge", Label: "A", Origin: "test", Metadata: map[string]any{"source": "internal"}})
|
|
store.UpsertNode(model.Node{ID: "b", Kind: "knowledge", Label: "B", Origin: "test", Metadata: map[string]any{"source": "internal"}})
|
|
store.SetVector("a", []float64{1, 0})
|
|
store.SetVector("b", []float64{.9, .1})
|
|
broker.Publish(model.Activity{Type: "learning.scan.completed", Source: "brain", Message: "done", Metadata: map[string]any{"run_id": "scan-1", "result": "updated"}})
|
|
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for {
|
|
history, err := store.AnalysisHistory(context.Background(), time.Now().Add(-time.Hour), 50)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(history.Events) > 0 {
|
|
if history.Totals.NodesCreated != 2 || history.Totals.VectorsCreated != 2 {
|
|
t.Fatalf("unexpected mutation totals: %+v", history.Totals)
|
|
}
|
|
if history.ChangeCount != 4 || len(history.Changes) != 4 {
|
|
t.Fatalf("expected four detailed changes, count=%d changes=%+v", history.ChangeCount, history.Changes)
|
|
}
|
|
if history.Events[0].ChangeCount != 4 {
|
|
t.Fatalf("expected event to own four changes: %+v", history.Events[0])
|
|
}
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("analysis writer did not persist event")
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func TestBuildAnalysisRunsExplainsPositiveThinkingResult(t *testing.T) {
|
|
start := time.Now().UTC().Add(-2 * time.Second)
|
|
events := []AnalysisEventRecord{
|
|
{Activity: model.Activity{ID: "s", Type: "think.cycle.started", Timestamp: start, Metadata: map[string]any{"trigger": "manual"}}},
|
|
{Activity: model.Activity{ID: "r", Type: "think.relation.created", Timestamp: start.Add(time.Second), Message: "Relation erstellt"}, Point: AnalysisPoint{Delta: MutationStats{EdgesCreated: 1}}},
|
|
{Activity: model.Activity{ID: "e", Type: "think.cycle.completed", Timestamp: start.Add(1500 * time.Millisecond), Metadata: map[string]any{"duration_ms": 1500, "relations_created": 1}}},
|
|
}
|
|
runs := buildAnalysisRuns(events)
|
|
if len(runs) != 1 {
|
|
t.Fatalf("expected one run, got %d: %+v", len(runs), runs)
|
|
}
|
|
if runs[0].Status != "success" || runs[0].Mutations.EdgesCreated != 1 || runs[0].Verdict != "positives Ergebnis" {
|
|
t.Fatalf("unexpected run: %+v", runs[0])
|
|
}
|
|
}
|
|
|
|
func TestEdgeUpdateChangeExplainsSimilarityRecalculation(t *testing.T) {
|
|
previous := model.Edge{ID: "e", Type: "related", Origin: "ai-inference", Status: "active", Confidence: .72, Metadata: map[string]any{"semantic_similarity": .81}}
|
|
current := model.Edge{ID: "e", Type: "related", Origin: "ai-inference", Status: "active", Confidence: .88, Metadata: map[string]any{"semantic_similarity": .93}}
|
|
change := edgeUpdateChange(previous, current)
|
|
if change.Action != "updated" || change.Details["previous_semantic_similarity"] != .81 || change.Details["semantic_similarity"] != .93 {
|
|
t.Fatalf("similarity delta missing: %+v", change)
|
|
}
|
|
if change.Details["previous_confidence"] != .72 || change.Details["confidence"] != .88 {
|
|
t.Fatalf("confidence delta missing: %+v", change)
|
|
}
|
|
}
|
|
|
|
func TestBuildAnalysisRunsTreatsResearchFetchAsNestedEvents(t *testing.T) {
|
|
start := time.Now().UTC().Add(-3 * time.Second)
|
|
meta := map[string]any{"research_id": "r-1", "research_query": "evidence tools"}
|
|
events := []AnalysisEventRecord{
|
|
{Activity: model.Activity{ID: "s", Type: "article.research.started", Timestamp: start, Metadata: meta}},
|
|
{Activity: model.Activity{ID: "f1s", Type: "article.research.fetch.started", Timestamp: start.Add(100 * time.Millisecond), Metadata: meta}},
|
|
{Activity: model.Activity{ID: "f1e", Type: "article.research.fetch.completed", Timestamp: start.Add(200 * time.Millisecond), Metadata: meta}},
|
|
{Activity: model.Activity{ID: "f2s", Type: "article.research.fetch.started", Timestamp: start.Add(300 * time.Millisecond), Metadata: meta}},
|
|
{Activity: model.Activity{ID: "f2e", Type: "article.research.fetch.completed", Timestamp: start.Add(400 * time.Millisecond), Metadata: meta}},
|
|
{Activity: model.Activity{ID: "e", Type: "article.research.completed", Timestamp: start.Add(time.Second), Metadata: meta}},
|
|
}
|
|
runs := buildAnalysisRuns(events)
|
|
if len(runs) != 1 {
|
|
t.Fatalf("expected exactly one research run, got %d: %+v", len(runs), runs)
|
|
}
|
|
if runs[0].EventCount != len(events) || runs[0].Status == "running" {
|
|
t.Fatalf("nested fetch lifecycle was not grouped correctly: %+v", runs[0])
|
|
}
|
|
}
|
|
|
|
func TestBuildAnalysisRunsDoesNotDuplicateActiveOrderForRepeatedStart(t *testing.T) {
|
|
start := time.Now().UTC().Add(-time.Second)
|
|
meta := map[string]any{"research_id": "r-dup", "research_query": "same query"}
|
|
events := []AnalysisEventRecord{
|
|
{Activity: model.Activity{ID: "s1", Type: "article.research.started", Timestamp: start, Metadata: meta}},
|
|
{Activity: model.Activity{ID: "s2", Type: "article.research.started", Timestamp: start.Add(10 * time.Millisecond), Metadata: meta}},
|
|
}
|
|
runs := buildAnalysisRuns(events)
|
|
if len(runs) != 1 {
|
|
t.Fatalf("expected one active run for duplicate start ID, got %d: %+v", len(runs), runs)
|
|
}
|
|
if runs[0].EventCount != 2 || runs[0].Status != "running" {
|
|
t.Fatalf("unexpected duplicate-start aggregation: %+v", runs[0])
|
|
}
|
|
}
|
|
|
|
func TestBuildAnalysisRunsClosesStaleLegacyRelationResearchFromResults(t *testing.T) {
|
|
start := time.Now().UTC().Add(-5 * time.Minute)
|
|
meta := map[string]any{"research_id": "legacy-r", "research_query": "legacy query"}
|
|
events := []AnalysisEventRecord{
|
|
{Activity: model.Activity{ID: "s", Type: "research.started", Timestamp: start, Metadata: meta}},
|
|
{Activity: model.Activity{ID: "r", Type: "research.results", Timestamp: start.Add(time.Second), Metadata: meta}},
|
|
}
|
|
runs := buildAnalysisRuns(events)
|
|
if len(runs) != 1 {
|
|
t.Fatalf("expected one legacy run, got %d: %+v", len(runs), runs)
|
|
}
|
|
if runs[0].Status == "running" || runs[0].Verdict != "abgeschlossen (Legacy)" {
|
|
t.Fatalf("stale legacy research should not remain running: %+v", runs[0])
|
|
}
|
|
}
|
|
|
|
func TestBuildAnalysisRunsKeepsRelationResearchOpenUntilExplicitCompletion(t *testing.T) {
|
|
start := time.Now().UTC().Add(-3 * time.Second)
|
|
meta := map[string]any{"research_id": "rel-r", "research_query": "relation query"}
|
|
events := []AnalysisEventRecord{
|
|
{Activity: model.Activity{ID: "s", Type: "research.started", Timestamp: start, Metadata: meta}},
|
|
{Activity: model.Activity{ID: "res", Type: "research.results", Timestamp: start.Add(200 * time.Millisecond), Metadata: meta}},
|
|
{Activity: model.Activity{ID: "ing", Type: "research.ingested", Timestamp: start.Add(300 * time.Millisecond), Metadata: meta}},
|
|
{Activity: model.Activity{ID: "end", Type: "research.completed", Timestamp: start.Add(time.Second), Metadata: meta}},
|
|
}
|
|
runs := buildAnalysisRuns(events)
|
|
if len(runs) != 1 {
|
|
t.Fatalf("expected one relation research run, got %d: %+v", len(runs), runs)
|
|
}
|
|
if runs[0].EventCount != 4 || runs[0].DurationMS < 900 {
|
|
t.Fatalf("research.ingested ended the run too early: %+v", runs[0])
|
|
}
|
|
}
|