package graph import ( "math" "testing" "github.com/local/glpi-neural-brain/internal/model" "github.com/local/glpi-neural-brain/internal/vectorgraph" ) func TestNextVectorNeighborPairScopedPrefersReciprocalAndSkipsReviewed(t *testing.T) { s := &Store{ nodes: map[string]model.Node{ "a": {ID: "a", Kind: "knowledge", Status: "production", Label: "A"}, "b": {ID: "b", Kind: "knowledge", Status: "production", Label: "B"}, "c": {ID: "c", Kind: "knowledge", Status: "production", Label: "C"}, }, edges: map[string]model.Edge{ "ab": {ID: "ab", Source: "a", Target: "b", Type: "semantic_neighbor", Origin: VectorMathOrigin, Status: "staging", Confidence: .93, Metadata: map[string]any{"semantic_similarity": .91, "reciprocal": false}}, "ac": {ID: "ac", Source: "a", Target: "c", Type: "semantic_neighbor", Origin: VectorMathOrigin, Status: "staging", Confidence: .90, Metadata: map[string]any{"semantic_similarity": .89, "reciprocal": true}}, }, vectors: map[string][]float32{}, } a, b, _, ok, _ := s.NextVectorNeighborPairScoped(NodeFilter{}, 2) if !ok || pairKey(a.ID, b.ID) != pairKey("a", "c") { t.Fatalf("expected reciprocal a-c candidate first, got %s-%s ok=%v", a.ID, b.ID, ok) } s.edges["reviewed"] = model.Edge{ID: "reviewed", Source: "a", Target: "c", Type: "related_to", Origin: "ai-inference", Status: "rejected"} a, b, _, ok, stats := s.NextVectorNeighborPairScoped(NodeFilter{}, 2) if !ok || pairKey(a.ID, b.ID) != pairKey("a", "b") { t.Fatalf("expected unreviewed a-b after a-c review, got %s-%s ok=%v stats=%+v", a.ID, b.ID, ok, stats) } if stats.AlreadyReviewed != 1 { t.Fatalf("expected one reviewed vector candidate, got %+v", stats) } } func TestKnowledgeOrphanIDsIgnoringOriginCountsExternalEvidenceBothDirections(t *testing.T) { s := &Store{ nodes: map[string]model.Node{ "k1": {ID: "k1", Kind: "knowledge", Status: "production"}, "k2": {ID: "k2", Kind: "knowledge", Status: "production"}, "x": {ID: "x", Kind: "external", Status: "research"}, }, edges: map[string]model.Edge{ "xk": {ID: "xk", Source: "x", Target: "k1", Type: "research_evidence", Origin: "research", Status: "verified"}, }, vectors: map[string][]float32{"k1": {1, 0}, "k2": {0, 1}}, } orphans := s.KnowledgeOrphanIDsIgnoringOrigin(NodeFilter{}, VectorMathOrigin) if len(orphans) != 1 || orphans[0] != "k2" { t.Fatalf("external->knowledge evidence must connect k1, got orphans=%v", orphans) } } func TestApplyVectorPositionsRelaxedCapsMovementAndSkipsTinyNoise(t *testing.T) { s := &Store{ nodes: map[string]model.Node{ "a": {ID: "a", Kind: "knowledge", Status: "production", X: .2, Y: .1, Z: 0}, }, 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{}, } before := s.nodes["a"] stats := s.applyVectorPositionsRelaxed([]vectorgraph.Position{{ID: "a", X: .8, Y: .5, Z: .3}}, .5, .01) if stats.NodesUpdated != 1 { t.Fatalf("expected one relaxed position update, got %+v", stats) } after := s.nodes["a"] d := math.Sqrt(math.Pow(after.X-before.X, 2) + math.Pow(after.Y-before.Y, 2) + math.Pow(after.Z-before.Z, 2)) if d > .010001 || d < .009 { t.Fatalf("movement must be capped near 0.01, got %.6f", d) } // A target close enough that the blended delta is below the no-op threshold // must not dirty the graph merely because of floating point/layout noise. tx, ty, tz := fitVectorPosition("a", after.X+.001, after.Y, after.Z) stats = s.applyVectorPositionsRelaxed([]vectorgraph.Position{{ID: "a", X: tx, Y: ty, Z: tz}}, .08, .035) if stats.NodesUpdated != 0 { t.Fatalf("sub-threshold relaxation should not dirty the graph, got %+v", stats) } }