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