mindsicle_test.go raw
1 package mindsicle
2
3 import (
4 "bytes"
5 "testing"
6
7 "git.mleku.dev/mleku/dendrite/pkg/axiom"
8 "git.mleku.dev/mleku/dendrite/pkg/lattice"
9 "git.mleku.dev/mleku/dendrite/pkg/spore"
10 )
11
12 type testConstraint struct{ tag string }
13
14 func (c testConstraint) Tag() string { return c.tag }
15 func (c testConstraint) Admits(e axiom.Element) bool { return e.Type() == c.tag }
16
17 type testElement struct {
18 typ string
19 val string
20 }
21
22 func (e testElement) Type() string { return e.typ }
23 func (e testElement) Value() any { return e.val }
24
25 func testConstraintFactory(tag string) axiom.Constraint {
26 return testConstraint{tag}
27 }
28
29 func TestFreezeThawRoundTrip(t *testing.T) {
30 // Build a small lattice: 3 nodes, 2 edges, 2 occupied.
31 l := lattice.New()
32 n0 := l.AddNode([]axiom.Constraint{testConstraint{"word"}})
33 n1 := l.AddNode([]axiom.Constraint{testConstraint{"word"}})
34 n2 := l.AddNode([]axiom.Constraint{testConstraint{"number"}})
35 l.Connect(n0, n1)
36 l.Connect(n1, n2)
37
38 n0.Bond(testElement{"word", "hello"})
39 n1.Bond(testElement{"word", "world"})
40 n0.SetPermutation(2)
41 n0.SetProjection(3, 5, 42)
42 n0.SetEnergy(true)
43
44 // Increment age on n0 twice.
45 n0.IncrementAge()
46 n0.IncrementAge()
47
48 // Create spore.
49 sp := spore.Extract(l)
50
51 // Freeze.
52 m := Freeze(l, sp)
53 if len(m.Nodes) != 3 {
54 t.Fatalf("expected 3 nodes, got %d", len(m.Nodes))
55 }
56 if m.Version != Version {
57 t.Fatalf("expected version %d, got %d", Version, m.Version)
58 }
59
60 // Serialize → deserialize.
61 var buf bytes.Buffer
62 if _, err := m.WriteTo(&buf); err != nil {
63 t.Fatalf("WriteTo: %v", err)
64 }
65 m2, err := ReadMindsicle(&buf)
66 if err != nil {
67 t.Fatalf("ReadMindsicle: %v", err)
68 }
69 if len(m2.Nodes) != 3 {
70 t.Fatalf("deserialized: expected 3 nodes, got %d", len(m2.Nodes))
71 }
72
73 // Thaw.
74 l2 := m2.Thaw(testConstraintFactory)
75 if l2.Size() != 3 {
76 t.Fatalf("thawed lattice size: expected 3, got %d", l2.Size())
77 }
78
79 // Verify occupants.
80 tn0 := l2.Node(0)
81 if !tn0.Occupied() {
82 t.Fatal("node 0 should be occupied")
83 }
84 if tn0.Occupant().Type() != "word" {
85 t.Fatalf("node 0 type: expected 'word', got %q", tn0.Occupant().Type())
86 }
87 if tn0.Occupant().Value().(string) != "hello" {
88 t.Fatalf("node 0 value: expected 'hello', got %v", tn0.Occupant().Value())
89 }
90
91 // Verify vacancy.
92 tn2 := l2.Node(2)
93 if tn2.Occupied() {
94 t.Fatal("node 2 should be vacant")
95 }
96
97 // Verify connectivity: node 1 should have 2 neighbors.
98 tn1 := l2.Node(1)
99 if len(tn1.Neighbors()) != 2 {
100 t.Fatalf("node 1 neighbors: expected 2, got %d", len(tn1.Neighbors()))
101 }
102
103 // Verify age survives round-trip.
104 if tn0.Age() != 2 {
105 t.Fatalf("node 0 age: expected 2, got %d", tn0.Age())
106 }
107
108 // Verify permutation.
109 if tn0.Permutation() != 2 {
110 t.Fatalf("node 0 perm: expected 2, got %d", tn0.Permutation())
111 }
112
113 // Verify projection.
114 if tn0.ProjectionVertex() != 3 {
115 t.Fatalf("node 0 proj vertex: expected 3, got %d", tn0.ProjectionVertex())
116 }
117 if tn0.ProjectionKey() != 5 {
118 t.Fatalf("node 0 proj key: expected 5, got %d", tn0.ProjectionKey())
119 }
120 if tn0.ProjectionPath() != 42 {
121 t.Fatalf("node 0 proj path: expected 42, got %d", tn0.ProjectionPath())
122 }
123
124 // Verify bond count.
125 if tn0.BondCount() != 1 {
126 t.Fatalf("node 0 bond count: expected 1, got %d", tn0.BondCount())
127 }
128 }
129
130 func TestEmptyLattice(t *testing.T) {
131 l := lattice.New()
132 m := Freeze(l, nil)
133 if len(m.Nodes) != 0 {
134 t.Fatalf("expected 0 nodes, got %d", len(m.Nodes))
135 }
136
137 var buf bytes.Buffer
138 m.WriteTo(&buf)
139 m2, err := ReadMindsicle(&buf)
140 if err != nil {
141 t.Fatalf("ReadMindsicle: %v", err)
142 }
143
144 l2 := m2.Thaw(testConstraintFactory)
145 if l2.Size() != 0 {
146 t.Fatalf("thawed empty lattice size: expected 0, got %d", l2.Size())
147 }
148 }
149
150 func TestVacantNodes(t *testing.T) {
151 l := lattice.New()
152 l.AddNode([]axiom.Constraint{testConstraint{"word"}})
153 l.AddNode([]axiom.Constraint{testConstraint{"number"}})
154
155 m := Freeze(l, nil)
156
157 var buf bytes.Buffer
158 m.WriteTo(&buf)
159 m2, _ := ReadMindsicle(&buf)
160 l2 := m2.Thaw(testConstraintFactory)
161
162 if l2.Size() != 2 {
163 t.Fatalf("expected 2 nodes, got %d", l2.Size())
164 }
165 // Both vacant.
166 for i := range 2 {
167 if l2.Node(lattice.NodeID(i)).Occupied() {
168 t.Fatalf("node %d should be vacant", i)
169 }
170 }
171 // Constraints survived.
172 cs := l2.Node(0).Constraints()
173 if len(cs) != 1 || cs[0].Tag() != "word" {
174 t.Fatalf("node 0 constraints: expected [word], got %v", cs)
175 }
176 }
177
178 func TestCandidates(t *testing.T) {
179 l := lattice.New()
180 n := l.AddNode([]axiom.Constraint{testConstraint{"word"}})
181 n.AddCandidate(testElement{"word", "alpha"})
182 n.AddCandidate(testElement{"word", "beta"})
183
184 m := Freeze(l, nil)
185
186 var buf bytes.Buffer
187 m.WriteTo(&buf)
188 m2, _ := ReadMindsicle(&buf)
189 l2 := m2.Thaw(testConstraintFactory)
190
191 tn := l2.Node(0)
192 // Candidates are added via AddCandidate which checks constraints.
193 // frozenElement has type "word" which satisfies testConstraint{"word"}.
194 cands := tn.Candidates()
195 if len(cands) != 2 {
196 t.Fatalf("expected 2 candidates, got %d", len(cands))
197 }
198 types := map[string]bool{}
199 for _, c := range cands {
200 types[c.Value().(string)] = true
201 }
202 if !types["alpha"] || !types["beta"] {
203 t.Fatalf("candidates missing: got %v", types)
204 }
205 }
206