Files
glpi-neural-brain/internal/vectorgraph/vectorgraph_test.go
jbergner 94dbd4ccab
All checks were successful
release-tag / release-image (push) Successful in 2m32s
RC-4
2026-08-09 18:41:47 +02:00

146 lines
5.2 KiB
Go

package vectorgraph
import (
"fmt"
"math"
"reflect"
"testing"
)
func TestBuildLinksNearbyVectorsWithoutModelDependency(t *testing.T) {
entries := []Entry{
{ID: "a", Vector: []float32{1, 0, 0, 0}},
{ID: "b", Vector: []float32{.99, .02, 0, 0}},
{ID: "c", Vector: []float32{.98, -.01, 0, 0}},
{ID: "x", Vector: []float32{0, 1, 0, 0}},
{ID: "y", Vector: []float32{0, .99, .02, 0}},
}
cfg := Config{Neighbors: 2, CandidateLimit: 32, HashBits: 16, HashTables: 2, BandBits: 8, MinSimilarity: .8, MinAffinity: .2}
result := Build(entries, cfg)
if len(result.Links) == 0 {
t.Fatal("expected semantic links")
}
seenAB := false
for _, link := range result.Links {
if (link.Source == "a" && link.Target == "b") || (link.Source == "b" && link.Target == "a") {
seenAB = true
}
if (link.Source == "a" && link.Target == "x") || (link.Source == "x" && link.Target == "a") {
t.Fatalf("orthogonal vectors must not link: %+v", link)
}
}
if !seenAB {
t.Fatalf("expected a/b to be linked: %+v", result.Links)
}
}
func TestBuildIsDeterministicAndProducesFiniteLayout(t *testing.T) {
entries := []Entry{
{ID: "a", Vector: []float32{1, .2, .1, 0}},
{ID: "b", Vector: []float32{.95, .22, .08, 0}},
{ID: "c", Vector: []float32{0, 1, .1, .2}},
{ID: "d", Vector: []float32{0, .96, .12, .18}},
}
cfg := Config{Neighbors: 2, CandidateLimit: 32, HashBits: 16, HashTables: 2, BandBits: 8, MinSimilarity: .7, MinAffinity: .2, Layout: true}
a := Build(entries, cfg)
b := Build(entries, cfg)
if !reflect.DeepEqual(a.Links, b.Links) || !reflect.DeepEqual(a.Positions, b.Positions) {
t.Fatal("vector graph must be deterministic")
}
if len(a.Positions) != len(entries) {
t.Fatalf("expected %d positions, got %d", len(entries), len(a.Positions))
}
for _, p := range a.Positions {
if math.IsNaN(p.X) || math.IsNaN(p.Y) || math.IsNaN(p.Z) || math.IsInf(p.X, 0) || math.IsInf(p.Y, 0) || math.IsInf(p.Z, 0) {
t.Fatalf("invalid position: %+v", p)
}
}
}
func TestBuildFocusedOnlyAnchorsRequestedOrphansAgainstFullCorpus(t *testing.T) {
entries := []Entry{
{ID: "connected-a", Vector: []float32{1, 0, 0, 0}},
{ID: "connected-b", Vector: []float32{.99, .01, 0, 0}},
{ID: "orphan", Vector: []float32{.985, .015, 0, 0}},
{ID: "far", Vector: []float32{0, 1, 0, 0}},
}
cfg := Config{Neighbors: 2, CandidateLimit: 32, HashBits: 16, HashTables: 2, BandBits: 8, MinSimilarity: .8, MinAffinity: .2}
result := BuildFocused(entries, map[string]bool{"orphan": true}, cfg)
if result.Stats.Focused != 1 {
t.Fatalf("expected one focused entry, got %+v", result.Stats)
}
if len(result.Links) == 0 {
t.Fatal("expected focused orphan to link into full corpus")
}
for _, link := range result.Links {
if link.Source != "orphan" && link.Target != "orphan" {
t.Fatalf("second pass must not invent links between non-focused nodes: %+v", link)
}
if link.Source == "far" || link.Target == "far" {
t.Fatalf("orthogonal candidate must remain unlinked: %+v", link)
}
}
}
func TestBuildFocusedIsDeterministic(t *testing.T) {
entries := []Entry{
{ID: "a", Vector: []float32{1, .1, 0, 0}},
{ID: "b", Vector: []float32{.99, .11, 0, 0}},
{ID: "c", Vector: []float32{.97, .13, 0, 0}},
}
cfg := Config{Neighbors: 2, CandidateLimit: 32, HashBits: 16, HashTables: 2, BandBits: 8, MinSimilarity: .7, MinAffinity: .1}
focus := map[string]bool{"c": true}
a := BuildFocused(entries, focus, cfg)
b := BuildFocused(entries, focus, cfg)
if !reflect.DeepEqual(a, b) {
t.Fatalf("focused vector graph must be deterministic: a=%+v b=%+v", a, b)
}
}
func TestSpreadDenseLayoutSeparatesDenseCellDeterministically(t *testing.T) {
entries := make([]indexedEntry, 24)
positions := make([][3]float64, 24)
for i := range entries {
entries[i] = indexedEntry{Entry: Entry{ID: fmt.Sprintf("node-%02d", i)}}
positions[i] = [3]float64{.1, .1, .1}
}
a := spreadDenseLayout(entries, append([][3]float64(nil), positions...))
b := spreadDenseLayout(entries, append([][3]float64(nil), positions...))
if !reflect.DeepEqual(a, b) {
t.Fatal("dense layout relaxation must be deterministic")
}
unique := map[[3]float64]bool{}
for _, p := range a {
unique[p] = true
}
if len(unique) < 8 {
t.Fatalf("dense cell was not meaningfully spread, unique positions=%d", len(unique))
}
}
func TestParallelWorkersMatchSingleWorker(t *testing.T) {
entries := make([]Entry, 96)
for i := range entries {
entries[i] = Entry{ID: fmt.Sprintf("n-%03d", i), Vector: []float32{
float32((i%11)+1) / 11,
float32((i%7)+1) / 7,
float32((i%5)+1) / 5,
float32((i%3)+1) / 3,
}}
}
base := Config{Workers: 1, Neighbors: 4, CandidateLimit: 48, HashBits: 16, HashTables: 2, BandBits: 8, MinSimilarity: .65, MinAffinity: .1, Layout: true}
parallel := base
parallel.Workers = 8
one := Build(entries, base)
many := Build(entries, parallel)
if !reflect.DeepEqual(one, many) {
t.Fatalf("parallel primary build differs from single-worker build")
}
focus := map[string]bool{"n-003": true, "n-017": true, "n-044": true, "n-081": true}
oneFocused := BuildFocused(entries, focus, base)
manyFocused := BuildFocused(entries, focus, parallel)
if !reflect.DeepEqual(oneFocused, manyFocused) {
t.Fatalf("parallel focused build differs from single-worker build")
}
}