cast_test.go raw
1 package oracle
2
3 import (
4 "testing"
5
6 "git.mleku.dev/mleku/dendrite/pkg/state"
7 )
8
9 func TestAbiogenesis(t *testing.T) {
10 o := New(42)
11 r := o.Cast("self", 0, 1)
12
13 if r == nil {
14 t.Fatal("Cast returned nil")
15 }
16 if r.Sequence != 0 {
17 t.Errorf("first reading should be sequence 0, got %d", r.Sequence)
18 }
19 if r.Generation != 1 {
20 t.Errorf("expected generation 1, got %d", r.Generation)
21 }
22 if r.Source != "self" {
23 t.Errorf("expected source 'self', got %q", r.Source)
24 }
25 if len(r.Directives) == 0 {
26 t.Error("expected at least one directive")
27 }
28
29 // Verify resulting hexagram matches applying changing lines.
30 expected := ApplyChangingLines(r.Primary, r.Lines)
31 if r.Resulting != expected {
32 t.Errorf("resulting hexagram %d != expected %d from ApplyChangingLines", r.Resulting, expected)
33 }
34 }
35
36 func TestChaining(t *testing.T) {
37 o := New(123)
38 r1 := o.Cast("self", 0, 1)
39 r2 := o.Cast("self", 1, 2)
40
41 // The second reading's primary must be the first reading's resulting.
42 if r2.Primary != r1.Resulting {
43 t.Errorf("chained primary %d != previous resulting %d", r2.Primary, r1.Resulting)
44 }
45 if r2.Sequence != 1 {
46 t.Errorf("expected sequence 1, got %d", r2.Sequence)
47 }
48
49 // First reading should be in history.
50 if len(o.History) != 1 {
51 t.Fatalf("expected 1 history entry, got %d", len(o.History))
52 }
53 if o.History[0] != r1 {
54 t.Error("history[0] is not the first reading")
55 }
56 }
57
58 func TestChain10(t *testing.T) {
59 o := New(999)
60 var readings []*Reading
61 for i := range 10 {
62 r := o.Cast("self", uint8(i%4), uint32(i+1))
63 readings = append(readings, r)
64 }
65
66 // Each reading's primary == previous reading's resulting.
67 for i := 1; i < len(readings); i++ {
68 if readings[i].Primary != readings[i-1].Resulting {
69 t.Errorf("reading %d primary %d != reading %d resulting %d",
70 i, readings[i].Primary, i-1, readings[i-1].Resulting)
71 }
72 }
73
74 // All resulting hexagrams must be valid (0-63).
75 for i, r := range readings {
76 if r.Resulting > 63 {
77 t.Errorf("reading %d resulting hexagram %d > 63", i, r.Resulting)
78 }
79 if r.Primary > 63 {
80 t.Errorf("reading %d primary hexagram %d > 63", i, r.Primary)
81 }
82 }
83 }
84
85 func TestXORReversibility(t *testing.T) {
86 // XOR is its own inverse: (a ^ b) ^ b == a
87 for a := LineState(0); a < 4; a++ {
88 for b := LineState(0); b < 4; b++ {
89 result := (a ^ b) ^ b
90 if result != a {
91 t.Errorf("XOR not reversible: (%d ^ %d) ^ %d = %d, want %d", a, b, b, result, a)
92 }
93 }
94 }
95 }
96
97 func TestXORTransitions(t *testing.T) {
98 // Verify the XOR transition semantics.
99 tests := []struct {
100 initial LineState
101 entropy LineState
102 expected LineState
103 desc string
104 }{
105 {YoungYin, 0b00, YoungYin, "00 entropy preserves state"},
106 {YoungYang, 0b00, YoungYang, "00 entropy preserves yang"},
107 {OldYin, 0b00, OldYin, "00 entropy preserves old yin"},
108 {OldYang, 0b00, OldYang, "00 entropy preserves old yang"},
109
110 {YoungYin, 0b01, YoungYang, "01 flips polarity yin→yang"},
111 {YoungYang, 0b01, YoungYin, "01 flips polarity yang→yin"},
112
113 {YoungYin, 0b10, OldYin, "10 flips stability young→old"},
114 {YoungYang, 0b10, OldYang, "10 flips stability young→old yang"},
115
116 {YoungYin, 0b11, OldYang, "11 inverts both"},
117 {OldYang, 0b11, YoungYin, "11 inverts old yang back"},
118 }
119
120 for _, tt := range tests {
121 got := tt.initial ^ tt.entropy
122 if got != tt.expected {
123 t.Errorf("%s: %d ^ %d = %d, want %d", tt.desc, tt.initial, tt.entropy, got, tt.expected)
124 }
125 }
126 }
127
128 func TestLineStatePredicates(t *testing.T) {
129 if YoungYin.IsChanging() {
130 t.Error("YoungYin should not be changing")
131 }
132 if YoungYang.IsChanging() {
133 t.Error("YoungYang should not be changing")
134 }
135 if !OldYin.IsChanging() {
136 t.Error("OldYin should be changing")
137 }
138 if !OldYang.IsChanging() {
139 t.Error("OldYang should be changing")
140 }
141
142 if YoungYin.IsYang() {
143 t.Error("YoungYin should not be yang")
144 }
145 if !YoungYang.IsYang() {
146 t.Error("YoungYang should be yang")
147 }
148 if OldYin.IsYang() {
149 t.Error("OldYin should not be yang")
150 }
151 if !OldYang.IsYang() {
152 t.Error("OldYang should be yang")
153 }
154 }
155
156 func TestStabilize(t *testing.T) {
157 if OldYin.Stabilize() != YoungYin {
158 t.Errorf("OldYin.Stabilize() = %d, want YoungYin (%d)", OldYin.Stabilize(), YoungYin)
159 }
160 if OldYang.Stabilize() != YoungYang {
161 t.Errorf("OldYang.Stabilize() = %d, want YoungYang (%d)", OldYang.Stabilize(), YoungYang)
162 }
163 // Young states stabilize to themselves.
164 if YoungYin.Stabilize() != YoungYin {
165 t.Error("YoungYin.Stabilize() should be identity")
166 }
167 if YoungYang.Stabilize() != YoungYang {
168 t.Error("YoungYang.Stabilize() should be identity")
169 }
170 }
171
172 func TestApplyChangingLines(t *testing.T) {
173 // No changing lines → resulting == primary.
174 hex := state.Hex(state.Heaven, state.Earth) // 101 inner, 000 outer
175 lines := [6]LineState{YoungYang, YoungYin, YoungYang, YoungYin, YoungYin, YoungYin}
176 result := ApplyChangingLines(hex, lines)
177 if result != hex {
178 t.Errorf("no changing lines: got %d, want %d", result, hex)
179 }
180
181 // Single changing inner line 0 (bonding bit).
182 lines[0] = OldYang // yang becoming yin → flip inner bit 0
183 result = ApplyChangingLines(hex, lines)
184 expected := hex.MoveLine(true, 0)
185 if result != expected {
186 t.Errorf("changing inner line 0: got %d, want %d", result, expected)
187 }
188
189 // Single changing outer line 3 (outer bit 0 = bonding).
190 lines[0] = YoungYang // reset
191 lines[3] = OldYin // yin becoming yang → flip outer bit 0
192 result = ApplyChangingLines(hex, lines)
193 expected = hex.MoveLine(false, 0)
194 if result != expected {
195 t.Errorf("changing outer line 3: got %d, want %d", result, expected)
196 }
197 }
198
199 func TestChangingLines(t *testing.T) {
200 r := &Reading{
201 Lines: [6]LineState{YoungYin, OldYang, YoungYang, OldYin, YoungYin, OldYang},
202 }
203 cl := r.ChangingLines()
204 expected := []int{1, 3, 5}
205 if len(cl) != len(expected) {
206 t.Fatalf("ChangingLines length %d, want %d", len(cl), len(expected))
207 }
208 for i, v := range cl {
209 if v != expected[i] {
210 t.Errorf("ChangingLines[%d] = %d, want %d", i, v, expected[i])
211 }
212 }
213 }
214
215 func TestDeterminism(t *testing.T) {
216 // Same seed + source + sequence should produce identical readings.
217 o1 := New(777)
218 r1 := o1.Cast("self", 0, 1)
219
220 o2 := New(777)
221 r2 := o2.Cast("self", 0, 1)
222
223 if r1.Primary != r2.Primary {
224 t.Error("determinism failed: different primaries")
225 }
226 if r1.Lines != r2.Lines {
227 t.Error("determinism failed: different lines")
228 }
229 if r1.Resulting != r2.Resulting {
230 t.Error("determinism failed: different resulting")
231 }
232 }
233
234 func TestMarshalRoundtrip(t *testing.T) {
235 o := New(42)
236 o.Cast("self", 0, 1)
237 o.Cast("forage", 1, 2)
238 o.Cast("nostr", 2, 3)
239
240 data, err := o.Marshal()
241 if err != nil {
242 t.Fatalf("Marshal: %v", err)
243 }
244
245 o2, err := FromState(data)
246 if err != nil {
247 t.Fatalf("FromState: %v", err)
248 }
249
250 if o2.Seed != o.Seed {
251 t.Errorf("seed mismatch: %d vs %d", o2.Seed, o.Seed)
252 }
253 if o2.Current.Sequence != o.Current.Sequence {
254 t.Errorf("current sequence mismatch: %d vs %d", o2.Current.Sequence, o.Current.Sequence)
255 }
256 if len(o2.History) != len(o.History) {
257 t.Errorf("history length mismatch: %d vs %d", len(o2.History), len(o.History))
258 }
259 }
260
261 func TestTrigramMapping(t *testing.T) {
262 // All 8 trigrams should map to distinct domain and intention types.
263 trigrams := []state.Trigram{
264 state.Earth, state.Thunder, state.Water, state.Lake,
265 state.Fire, state.Heaven, state.Wind, state.Mountain,
266 }
267
268 domains := make(map[DomainType]bool)
269 intentions := make(map[IntentionType]bool)
270
271 for _, tri := range trigrams {
272 d := TrigramToDomain(tri)
273 if domains[d] {
274 t.Errorf("duplicate domain %d for trigram %d", d, tri)
275 }
276 domains[d] = true
277
278 i := TrigramToIntention(tri)
279 if intentions[i] {
280 t.Errorf("duplicate intention %d for trigram %d", i, tri)
281 }
282 intentions[i] = true
283 }
284
285 if len(domains) != 8 {
286 t.Errorf("expected 8 distinct domains, got %d", len(domains))
287 }
288 if len(intentions) != 8 {
289 t.Errorf("expected 8 distinct intentions, got %d", len(intentions))
290 }
291 }
292
293 func TestDirectiveGeneration(t *testing.T) {
294 o := New(42)
295 r := o.Cast("forage", 0, 1)
296
297 if len(r.Directives) == 0 {
298 t.Fatal("expected at least one directive")
299 }
300
301 // First directive should be primary (ChangingBit == -1).
302 if r.Directives[0].ChangingBit != -1 {
303 t.Errorf("first directive ChangingBit = %d, want -1", r.Directives[0].ChangingBit)
304 }
305
306 // Number of directives should be 1 (primary) + number of changing lines.
307 changingCount := len(r.ChangingLines())
308 expected := 1 + changingCount
309 if len(r.Directives) != expected {
310 t.Errorf("directive count %d, want %d (1 primary + %d changing)",
311 len(r.Directives), expected, changingCount)
312 }
313
314 // Forage source should produce search-type directives.
315 for i, d := range r.Directives {
316 if d.Type != DirectiveSearch {
317 t.Errorf("directive %d type %d, want DirectiveSearch for forage source", i, d.Type)
318 }
319 }
320 }
321
322 func TestSelfDirectiveType(t *testing.T) {
323 o := New(42)
324 r := o.Cast("self", 0, 1)
325
326 for i, d := range r.Directives {
327 if d.Type != DirectiveWalkWeight {
328 t.Errorf("directive %d type %d, want DirectiveWalkWeight for self source", i, d.Type)
329 }
330 }
331 }
332