profile_test.go raw

   1  package profile
   2  
   3  import (
   4  	"testing"
   5  
   6  	"git.mleku.dev/mleku/dendrite/pkg/grow"
   7  	"git.mleku.dev/mleku/dendrite/pkg/lattice"
   8  	"git.mleku.dev/mleku/dendrite/pkg/ratio"
   9  )
  10  
  11  // mockElement implements axiom.Element for testing.
  12  type mockElement struct {
  13  	tag string
  14  	val any
  15  }
  16  
  17  func (e mockElement) Type() string { return e.tag }
  18  func (e mockElement) Value() any   { return e.val }
  19  
  20  func TestCollectorRecordsBonds(t *testing.T) {
  21  	c := NewCollector()
  22  
  23  	c.RecordGrowEvent(grow.Event{
  24  		Type:    grow.EventBonded,
  25  		NodeID:  1,
  26  		Element: mockElement{"word", "hello"},
  27  		Steps:   3,
  28  	})
  29  	c.RecordGrowEvent(grow.Event{
  30  		Type:    grow.EventBonded,
  31  		NodeID:  2,
  32  		Element: mockElement{"punct", "."},
  33  		Steps:   1,
  34  	})
  35  	c.RecordGrowEvent(grow.Event{
  36  		Type:    grow.EventRejected,
  37  		Element: mockElement{"word", "xyz"},
  38  	})
  39  
  40  	snap := c.Snapshot()
  41  
  42  	if snap.TokensIngested != 3 {
  43  		t.Errorf("tokens = %d, want 3", snap.TokensIngested)
  44  	}
  45  	if snap.BondEvents != 2 {
  46  		t.Errorf("bonds = %d, want 2", snap.BondEvents)
  47  	}
  48  	if snap.RejectEvents != 1 {
  49  		t.Errorf("rejects = %d, want 1", snap.RejectEvents)
  50  	}
  51  	if snap.PathFreq[1] != 1 || snap.PathFreq[2] != 1 {
  52  		t.Errorf("path freq wrong: %v", snap.PathFreq)
  53  	}
  54  	if snap.BondDist["word"] != 1 || snap.BondDist["punct"] != 1 {
  55  		t.Errorf("bond dist wrong: %v", snap.BondDist)
  56  	}
  57  	if snap.WalkDistHist[3] != 1 || snap.WalkDistHist[1] != 1 {
  58  		t.Errorf("walk dist wrong: %v", snap.WalkDistHist)
  59  	}
  60  }
  61  
  62  func TestCollectorTracksTransitions(t *testing.T) {
  63  	c := NewCollector()
  64  
  65  	tags := []string{"word", "space", "word", "punct"}
  66  	for i, tag := range tags {
  67  		c.RecordGrowEvent(grow.Event{
  68  			Type:    grow.EventBonded,
  69  			NodeID:  lattice.NodeID(i),
  70  			Element: mockElement{tag, tag},
  71  			Steps:   0,
  72  		})
  73  	}
  74  
  75  	snap := c.Snapshot()
  76  
  77  	// Should have 3 transitions: word->space, space->word, word->punct.
  78  	if len(snap.TransitionFreq) != 3 {
  79  		t.Errorf("transition count = %d, want 3", len(snap.TransitionFreq))
  80  	}
  81  	if snap.TransitionFreq[[2]string{"word", "space"}] != 1 {
  82  		t.Error("missing word->space transition")
  83  	}
  84  	if snap.TransitionFreq[[2]string{"space", "word"}] != 1 {
  85  		t.Error("missing space->word transition")
  86  	}
  87  	if snap.TransitionFreq[[2]string{"word", "punct"}] != 1 {
  88  		t.Error("missing word->punct transition")
  89  	}
  90  }
  91  
  92  func TestProfileClone(t *testing.T) {
  93  	p := New()
  94  	p.TokensIngested = 100
  95  	p.BondEvents = 50
  96  	p.PathFreq[1] = 10
  97  	p.BondDist["word"] = 40
  98  
  99  	c := p.Clone()
 100  	c.TokensIngested = 200
 101  	c.PathFreq[1] = 20
 102  
 103  	if p.TokensIngested != 100 {
 104  		t.Error("clone modified original tokens")
 105  	}
 106  	if p.PathFreq[1] != 10 {
 107  		t.Error("clone modified original path freq")
 108  	}
 109  }
 110  
 111  func TestProfileMarshalRoundtrip(t *testing.T) {
 112  	p := New()
 113  	p.TokensIngested = 42
 114  	p.BondEvents = 10
 115  	p.PathFreq[5] = 3
 116  
 117  	data, err := p.Marshal()
 118  	if err != nil {
 119  		t.Fatal(err)
 120  	}
 121  
 122  	p2, err := Unmarshal(data)
 123  	if err != nil {
 124  		t.Fatal(err)
 125  	}
 126  
 127  	if p2.TokensIngested != 42 {
 128  		t.Errorf("tokens = %d, want 42", p2.TokensIngested)
 129  	}
 130  	if p2.BondEvents != 10 {
 131  		t.Errorf("bonds = %d, want 10", p2.BondEvents)
 132  	}
 133  }
 134  
 135  func TestStatsCompute(t *testing.T) {
 136  	p := New()
 137  	p.TokensIngested = 1000
 138  	p.BondEvents = 800
 139  	p.NewVertices = 5
 140  
 141  	// Distribute bonds across some nodes.
 142  	for i := 0; i < 20; i++ {
 143  		p.PathFreq[lattice.NodeID(i)] = int64(i + 1)
 144  	}
 145  	p.WalkDistHist[0] = 400
 146  	p.WalkDistHist[1] = 200
 147  	p.WalkDistHist[5] = 150
 148  	p.WalkDistHist[10] = 50
 149  
 150  	s := Compute(p, 100)
 151  
 152  	if s.BondRate.IsZero() {
 153  		t.Error("bond rate is zero")
 154  	}
 155  	if !s.BondRate.Equal(ratio.New(800, 1000)) {
 156  		t.Errorf("bond rate = %s, want 4/5", s.BondRate.String())
 157  	}
 158  	if s.NewVertexRate.IsZero() {
 159  		t.Error("new vertex rate is zero")
 160  	}
 161  	if s.PathEntropy.IsZero() {
 162  		t.Error("path entropy is zero")
 163  	}
 164  	if s.VertexCoverage.IsZero() {
 165  		t.Error("vertex coverage is zero")
 166  	}
 167  	if !s.VertexCoverage.Equal(ratio.New(20, 100)) {
 168  		t.Errorf("vertex coverage = %s, want 1/5", s.VertexCoverage.String())
 169  	}
 170  	if s.BurstinessGini.IsZero() {
 171  		t.Error("gini is zero (should be non-zero for non-uniform distribution)")
 172  	}
 173  }
 174  
 175  func TestCompareIdentical(t *testing.T) {
 176  	s := Stats{
 177  		PathEntropy:       ratio.New(5, 1),
 178  		SurprisalVariance: ratio.New(3, 1),
 179  		BurstinessGini:    ratio.New(4, 10),
 180  		VertexCoverage:    ratio.New(2, 10),
 181  		AvgWalkDistance:   ratio.New(7, 1),
 182  		BondRate:          ratio.New(8, 10),
 183  		NewVertexRate:     ratio.New(1, 10000),
 184  	}
 185  
 186  	v := Compare(s, s)
 187  
 188  	if !v.HumanProbability.Equal(ratio.One) {
 189  		t.Errorf("identical stats should give probability 1, got %s", v.HumanProbability.String())
 190  	}
 191  }
 192  
 193  func TestCompareDivergent(t *testing.T) {
 194  	baseline := Stats{
 195  		PathEntropy:       ratio.New(10, 1),
 196  		SurprisalVariance: ratio.New(8, 1),
 197  		BurstinessGini:    ratio.New(6, 10),
 198  		VertexCoverage:    ratio.New(3, 10),
 199  		AvgWalkDistance:   ratio.New(5, 1),
 200  		BondRate:          ratio.New(9, 10),
 201  		NewVertexRate:     ratio.New(1, 100000),
 202  	}
 203  
 204  	// AI-like: lower entropy, lower variance, lower burstiness.
 205  	aiLike := Stats{
 206  		PathEntropy:       ratio.New(3, 1),
 207  		SurprisalVariance: ratio.New(2, 1),
 208  		BurstinessGini:    ratio.New(1, 10),
 209  		VertexCoverage:    ratio.New(3, 10),
 210  		AvgWalkDistance:   ratio.New(5, 1),
 211  		BondRate:          ratio.New(9, 10),
 212  		NewVertexRate:     ratio.New(1, 100000),
 213  	}
 214  
 215  	v := Compare(baseline, aiLike)
 216  
 217  	if v.HumanProbability.Greater(ratio.New(8, 10)) {
 218  		t.Errorf("AI-like text should have lower human probability, got %s",
 219  			v.HumanProbability.String())
 220  	}
 221  }
 222