package graph import ( "testing" "github.com/local/glpi-neural-brain/internal/model" ) func TestReplaceOriginsPreservesUnchangedVector(t *testing.T) { s, err := Open(t.TempDir()) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = s.Close() }) n := model.Node{ID: "n1", Kind: "knowledge", Label: "VPN", Summary: "Gateway", Origin: "knowledge-production"} s.ReplaceOrigins([]string{"knowledge-production"}, []model.Node{n}, nil) s.SetVector("n1", []float64{1, 2, 3}) s.ReplaceOrigins([]string{"knowledge-production"}, []model.Node{n}, nil) v, ok := s.Vector("n1") if !ok || len(v) != 3 { t.Fatalf("vector was not preserved: %v %v", ok, v) } n.Summary = "changed" s.ReplaceOrigins([]string{"knowledge-production"}, []model.Node{n}, nil) if _, ok := s.Vector("n1"); ok { t.Fatal("changed document retained stale vector") } } func TestRejectedEdgePreventsPairReprocessingButIsHidden(t *testing.T) { s, _ := Open(t.TempDir()) t.Cleanup(func() { _ = s.Close() }) a := model.Node{ID: "a", Kind: "knowledge", Label: "A", Origin: "knowledge-production"} b := model.Node{ID: "b", Kind: "knowledge", Label: "B", Origin: "knowledge-production"} s.UpsertNode(a) s.UpsertNode(b) s.SetVector("a", []float64{1, 0}) s.SetVector("b", []float64{.9, .1}) if _, _, _, ok := s.BestPair(.5); !ok { t.Fatal("expected candidate pair") } s.UpsertEdge(model.Edge{Source: "a", Target: "b", Type: "related_to", Origin: "ai-inference", Status: "rejected"}) if _, _, _, ok := s.BestPair(.5); ok { t.Fatal("rejected pair was selected again") } if got := len(s.Snapshot().Edges); got != 0 { t.Fatalf("rejected edge should not be rendered, got %d", got) } } func TestNextPairUsesBoundedRotatingAnchors(t *testing.T) { s, _ := Open(t.TempDir()) t.Cleanup(func() { _ = s.Close() }) for _, id := range []string{"a", "b", "c", "d"} { s.UpsertNode(model.Node{ID: id, Kind: "knowledge", Label: id, Origin: "knowledge-production"}) } s.SetVector("a", []float64{1, 0}) s.SetVector("b", []float64{.99, .01}) s.SetVector("c", []float64{0, 1}) s.SetVector("d", []float64{.01, .99}) a, b, _, ok, comparisons := s.NextPair(.8, 1) if !ok || comparisons == 0 { t.Fatalf("expected bounded candidate search, ok=%v comparisons=%d", ok, comparisons) } s.UpsertEdge(model.Edge{Source: a.ID, Target: b.ID, Type: "related_to", Origin: "ai-inference", Status: "rejected"}) _, _, _, _, comparisons = s.NextPair(.8, 1) if comparisons == 0 { t.Fatal("rotating anchor did not advance after rejected pair") } if _, _, _, ok, comparisons = s.NextPair(.8, 1); !ok || comparisons == 0 { t.Fatalf("rotating anchor did not reach the next semantic region, ok=%v comparisons=%d", ok, comparisons) } } func TestSourceFiltersLimitEmbeddingAndThinkingCandidates(t *testing.T) { s, err := Open(t.TempDir()) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = s.Close() }) nodes := []model.Node{ {ID: "internal-a", Kind: "knowledge", Label: "Internal A", Categories: []string{"Netzwerk"}, Origin: "test", Metadata: map[string]any{"source": "internal-category"}}, {ID: "internal-b", Kind: "knowledge", Label: "Internal B", Categories: []string{"Applikation"}, Origin: "test", Metadata: map[string]any{"source": "internal-category"}}, {ID: "glpi-a", Kind: "knowledge", Label: "GLPI A", Categories: []string{"Netzwerk"}, Origin: "glpi-kb", Metadata: map[string]any{"source": "GLPI Knowledge Base"}}, {ID: "none", Kind: "knowledge", Label: "Ohne", Origin: "test"}, } for _, node := range nodes { s.UpsertNode(node) } pending := s.NodesForEmbeddingFiltered([]string{"internal-category"}) if len(pending) != 2 { t.Fatalf("expected two internal source embeddings, got %d", len(pending)) } for _, node := range nodes { s.SetVector(node.ID, []float64{1, .01}) } a, b, _, ok, _ := s.NextPairFiltered(.5, 8, []string{"internal-category"}) if !ok || NodeSource(a) != "internal-category" || NodeSource(b) != "internal-category" { t.Fatalf("thinking source filter returned wrong pair: ok=%v a=%+v b=%+v", ok, a, b) } } func TestReplaceOriginsPreservesLegacyRuntimeArticleProvenance(t *testing.T) { s := &Store{ nodes: map[string]model.Node{}, edges: map[string]model.Edge{}, vectors: map[string][]float32{}, dirtyNodes: map[string]uint64{}, dirtyEdges: map[string]uint64{}, dirtyVectors: map[string]uint64{}, deletedNodes: map[string]uint64{}, deletedEdges: map[string]uint64{}, deletedVectors: map[string]uint64{}, } article := model.Node{ID: "article", Kind: "ai-think", Origin: "knowledge-staging", Label: "Article"} source := model.Node{ID: "source", Kind: "knowledge", Origin: "knowledge-production", Label: "Source"} s.UpsertNode(article) s.UpsertNode(source) legacy := model.Edge{Source: "article", Target: "source", Type: "synthesized_from", Origin: "knowledge-staging", Status: "staging"} s.UpsertEdge(legacy) // Incoming file-owned model intentionally has no synthesized_from edge. s.ReplaceOriginsWithStats([]string{"knowledge-production", "knowledge-staging", "knowledge-taxonomy"}, []model.Node{article, source}, nil) wanted := EdgeID("article", "source", "synthesized_from", "knowledge-staging") for _, edge := range s.Snapshot().Edges { if edge.ID == wanted { return } } t.Fatal("legacy runtime article provenance was removed by managed-origin reconciliation") } func TestReplaceOriginsDeletesRuntimeProvenanceWhenArticleIsActuallyRemoved(t *testing.T) { s := &Store{ nodes: map[string]model.Node{}, edges: map[string]model.Edge{}, vectors: map[string][]float32{}, dirtyNodes: map[string]uint64{}, dirtyEdges: map[string]uint64{}, dirtyVectors: map[string]uint64{}, deletedNodes: map[string]uint64{}, deletedEdges: map[string]uint64{}, deletedVectors: map[string]uint64{}, } article := model.Node{ID: "article", Kind: "ai-think", Origin: "knowledge-staging", Label: "Article"} source := model.Node{ID: "source", Kind: "knowledge", Origin: "knowledge-production", Label: "Source"} s.UpsertNode(article) s.UpsertNode(source) s.UpsertEdge(model.Edge{Source: "article", Target: "source", Type: "synthesized_from", Origin: "knowledge-synthesis", Status: "staging"}) stats := s.ReplaceOriginsWithStats([]string{"knowledge-production", "knowledge-staging", "knowledge-taxonomy"}, []model.Node{source}, nil) if stats.NodesDeleted != 1 || stats.EdgesDeleted != 1 { t.Fatalf("actual article deletion must cascade runtime provenance: %+v", stats) } if _, ok := s.GetNode("article"); ok { t.Fatal("article node still present") } for _, edge := range s.Snapshot().Edges { if edge.Source == "article" || edge.Target == "article" { t.Fatalf("dangling edge remained after article deletion: %+v", edge) } } } func TestReplaceOriginsPreservesLegacyArticleStructuralMetadata(t *testing.T) { s := &Store{ nodes: map[string]model.Node{}, edges: map[string]model.Edge{}, vectors: map[string][]float32{}, dirtyNodes: map[string]uint64{}, dirtyEdges: map[string]uint64{}, dirtyVectors: map[string]uint64{}, deletedNodes: map[string]uint64{}, deletedEdges: map[string]uint64{}, deletedVectors: map[string]uint64{}, } old := model.Node{ID: "article", Kind: "ai-think", Origin: "knowledge-staging", Label: "Article", Metadata: map[string]any{ "subtype": "knowledge_synthesis", "generation_depth": 1, "source_node_ids": []string{"a", "b"}, "source_fingerprint": "fp", }} s.UpsertNode(old) incoming := model.Node{ID: "article", Kind: "ai-think", Origin: "knowledge-staging", Label: "Article", Metadata: map[string]any{"path": "article.json"}} s.ReplaceOriginsWithStats([]string{"knowledge-staging"}, []model.Node{incoming}, nil) got, ok := s.GetNode("article") if !ok { t.Fatal("article missing") } if got.Metadata["subtype"] != "knowledge_synthesis" || int(got.Metadata["generation_depth"].(int)) != 1 || got.Metadata["source_fingerprint"] != "fp" { t.Fatalf("legacy structural metadata was lost: %+v", got.Metadata) } } func TestAnalyzeCountsExternalEvidenceInEitherDirection(t *testing.T) { s := &Store{ nodes: map[string]model.Node{}, edges: map[string]model.Edge{}, vectors: map[string][]float32{}, dirtyNodes: map[string]uint64{}, dirtyEdges: map[string]uint64{}, dirtyVectors: map[string]uint64{}, deletedNodes: map[string]uint64{}, deletedEdges: map[string]uint64{}, deletedVectors: map[string]uint64{}, } s.UpsertNode(model.Node{ID: "kb", Kind: "knowledge", Status: "production", Origin: "knowledge-production", Label: "systemd"}) s.UpsertNode(model.Node{ID: "ext", Kind: "external", Status: "research", Origin: "research", Label: "systemd advisory"}) s.UpsertEdge(model.Edge{Source: "ext", Target: "kb", Type: "research_evidence", Origin: "research", Status: "verified"}) analysis := s.Analyze() if analysis.KnowledgeOrphans != 0 { t.Fatalf("external -> knowledge evidence must count as knowledge connectivity, got %d orphan(s)", analysis.KnowledgeOrphans) } }