All checks were successful
release-tag / release-image (push) Successful in 2m43s
89 lines
3.8 KiB
Go
89 lines
3.8 KiB
Go
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)
|
|
}
|
|
}
|