dissolve_test.go raw
1 package dissolve
2
3 import (
4 "testing"
5
6 "git.mleku.dev/mleku/dendrite/pkg/axiom"
7 "git.mleku.dev/mleku/dendrite/pkg/enzyme"
8 "git.mleku.dev/mleku/dendrite/pkg/lattice"
9 "git.mleku.dev/mleku/dendrite/pkg/ratio"
10 )
11
12 type tagConstraint struct{ tag string }
13
14 func (c tagConstraint) Tag() string { return c.tag }
15 func (c tagConstraint) Admits(e axiom.Element) bool { return e.Type() == c.tag }
16
17 func TestDissolveWeakBonds(t *testing.T) {
18 l := lattice.New()
19
20 // One constraint = lock-in of 1.0 (survives threshold 0.5).
21 strong := l.AddNode([]axiom.Constraint{tagConstraint{"word"}})
22 strong.Bond(enzyme.Elem("word", "strong"))
23
24 // One constraint = lock-in of 1.0, but we'll test with high threshold.
25 weak := l.AddNode([]axiom.Constraint{tagConstraint{"word"}})
26 weak.Bond(enzyme.Elem("word", "weak"))
27
28 dissolved := make(chan axiom.Element, 10)
29 events := make(chan Event, 10)
30
31 // Threshold 1.5 — both should dissolve since lock-in is 1.0.
32 ScanOnce(l, Config{Threshold: ratio.New(3, 2)}, dissolved, events)
33
34 close(dissolved)
35 close(events)
36
37 count := 0
38 for range dissolved {
39 count++
40 }
41 if count != 2 {
42 t.Errorf("expected 2 dissolved, got %d", count)
43 }
44
45 if strong.Occupied() {
46 t.Error("strong should be dissolved at threshold 1.5")
47 }
48 if weak.Occupied() {
49 t.Error("weak should be dissolved at threshold 1.5")
50 }
51 }
52
53 func TestDissolveLeavesStrongBonds(t *testing.T) {
54 l := lattice.New()
55
56 // Two constraints = lock-in of 2.0.
57 n := l.AddNode([]axiom.Constraint{
58 tagConstraint{"word"},
59 lengthConstraint{"word", 3},
60 })
61 n.Bond(enzyme.Elem("word", "hello"))
62
63 // Add occupied neighbors so contextual lock-in stays high.
64 // ContextualLockIn = base * (0.3 + 0.7 * neighborOccupancyRate).
65 // Neighbors also need high enough lock-in to survive the scan themselves,
66 // otherwise they dissolve first and n loses support.
67 // Give neighbors 2 constraints each, and connect them to each other
68 // so everyone has occupied neighbors.
69 nb1 := l.AddNode([]axiom.Constraint{tagConstraint{"word"}, lengthConstraint{"word", 3}})
70 nb1.Bond(enzyme.Elem("word", "peer1"))
71 l.Connect(n, nb1)
72
73 nb2 := l.AddNode([]axiom.Constraint{tagConstraint{"word"}, lengthConstraint{"word", 3}})
74 nb2.Bond(enzyme.Elem("word", "peer2"))
75 l.Connect(n, nb2)
76
77 // Connect neighbors to each other for mutual support.
78 l.Connect(nb1, nb2)
79
80 dissolved := make(chan axiom.Element, 10)
81 events := make(chan Event, 10)
82
83 // Threshold 0.9 — contextual lock-in with all-occupied neighbors = 1.0.
84 // All three nodes have full neighbor support → contextual lock-in = 1.0 > 0.9.
85 ScanOnce(l, Config{Threshold: ratio.New(9, 10)}, dissolved, events)
86
87 close(dissolved)
88 close(events)
89
90 if !n.Occupied() {
91 t.Error("strongly bonded node should survive with neighbor support")
92 }
93 }
94
95 func TestDissolveReturnsToSolution(t *testing.T) {
96 l := lattice.New()
97 n := l.AddNode([]axiom.Constraint{tagConstraint{"word"}})
98 n.Bond(enzyme.Elem("word", "recycled"))
99
100 dissolved := make(chan axiom.Element, 10)
101 events := make(chan Event, 10)
102
103 ScanOnce(l, Config{Threshold: ratio.FromInt(2)}, dissolved, events)
104
105 close(dissolved)
106 close(events)
107
108 elem := <-dissolved
109 if elem == nil {
110 t.Fatal("expected dissolved element")
111 }
112 if elem.Value().(string) != "recycled" {
113 t.Errorf("expected 'recycled', got %v", elem.Value())
114 }
115 }
116
117 func TestDissolveEmptyLattice(t *testing.T) {
118 l := lattice.New()
119 dissolved := make(chan axiom.Element, 10)
120 events := make(chan Event, 10)
121
122 // Should not panic on empty lattice.
123 ScanOnce(l, Config{Threshold: ratio.One}, dissolved, events)
124
125 close(dissolved)
126 close(events)
127
128 count := 0
129 for range dissolved {
130 count++
131 }
132 if count != 0 {
133 t.Errorf("expected 0 dissolved from empty lattice, got %d", count)
134 }
135 }
136
137 // lengthConstraint admits elements with string values of at least minLen.
138 type lengthConstraint struct {
139 tag string
140 minLen int
141 }
142
143 func (c lengthConstraint) Tag() string { return c.tag }
144 func (c lengthConstraint) Admits(e axiom.Element) bool {
145 if e.Type() != c.tag {
146 return false
147 }
148 s, ok := e.Value().(string)
149 if !ok {
150 return false
151 }
152 return len(s) >= c.minLen
153 }
154