package hexagram import ( "context" "testing" "time" "git.mleku.dev/mleku/dendrite/pkg/axiom" "git.mleku.dev/mleku/dendrite/pkg/lattice" "git.mleku.dev/mleku/dendrite/pkg/state" ) type tagConstraint struct{ tag string } func (c tagConstraint) Tag() string { return c.tag } func (c tagConstraint) Admits(e axiom.Element) bool { return e.Type() == c.tag } type elem struct { tag string val any } func (e elem) Type() string { return e.tag } func (e elem) Value() any { return e.val } func TestTableCompleteness(t *testing.T) { // All 64 entries should be populated (no zero-value Op except OpNone). noneCount := 0 for i, rule := range table { if rule.Op == OpNone { noneCount++ } _ = i } // Some OpNone entries are expected (stable states), but not all. if noneCount == 64 { t.Fatal("all 64 rules are OpNone — table is empty") } t.Logf("table: %d OpNone, %d active", noneCount, 64-noneCount) } func TestLookupCreativeCreative(t *testing.T) { // Heaven/Heaven = Mountain inner (111), all bits set. // Inner Mountain (111) + outer Mountain (111) = full equilibrium. h := state.Hex(state.Mountain, state.Mountain) rule := Lookup(h) // Should be a low-priority maintenance op. if rule.Op == OpDissolve { t.Error("full equilibrium should not dissolve") } } func TestLookupEarthEarth(t *testing.T) { // Earth/Earth = completely empty, no energy. h := state.Hex(state.Earth, state.Earth) rule := Lookup(h) if rule.Op != OpNone { t.Errorf("Earth/Earth should be OpNone, got %d", rule.Op) } if rule.Priority != PriorityIdle { t.Errorf("Earth/Earth should be idle priority, got %d", rule.Priority) } } func TestLookupHeavenInner(t *testing.T) { // Heaven inner (101) = ideal growth. Should accrete. h := state.Hex(state.Heaven, state.Earth) rule := Lookup(h) if rule.Op != OpAccrete { t.Errorf("Heaven/Earth should accrete, got %d", rule.Op) } } func TestLookupFireInner(t *testing.T) { // Fire inner (100) = noisy growth, no constraint. // With constrained environment -> prune. h := state.Hex(state.Fire, state.Mountain) rule := Lookup(h) if rule.Op != OpPrune { t.Errorf("Fire/Mountain should prune, got %d", rule.Op) } } func TestEngineTick(t *testing.T) { l := lattice.New() // Create a small lattice. nodes := make([]*lattice.Node, 5) for i := range nodes { nodes[i] = l.AddNode([]axiom.Constraint{tagConstraint{"word"}}) } for i := range nodes { l.Connect(nodes[i], nodes[(i+1)%len(nodes)]) } // Set energy on some nodes to trigger operations. nodes[0].SetEnergy(true) nodes[1].SetEnergy(true) // Put elements in solution. solution := make(chan axiom.Element, 20) solution <- elem{"word", "hello"} solution <- elem{"word", "world"} events := make(chan Event, 100) ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel() go RunEngine(ctx, l, EngineConfig{ Interval: 10 * time.Millisecond, Solution: solution, MaxNewSites: 2, }, events) <-ctx.Done() close(events) opCounts := make(map[Op]int) for ev := range events { opCounts[ev.Op]++ } t.Logf("engine operations: %v", opCounts) // The engine should have done something. total := 0 for _, count := range opCounts { total += count } if total == 0 { t.Error("engine performed no operations") } } func TestEngineGrowsLattice(t *testing.T) { l := lattice.New() // Start with 3 nodes, all energized. nodes := make([]*lattice.Node, 3) for i := range nodes { nodes[i] = l.AddNode([]axiom.Constraint{tagConstraint{"word"}}) nodes[i].SetEnergy(true) } for i := range nodes { l.Connect(nodes[i], nodes[(i+1)%len(nodes)]) } // Bond one to make it Mountain (111) — which triggers Explore. nodes[0].Bond(elem{"word", "seed"}) initialSize := l.Size() events := make(chan Event, 200) ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond) defer cancel() go RunEngine(ctx, l, EngineConfig{ Interval: 10 * time.Millisecond, MaxNewSites: 4, }, events) <-ctx.Done() finalSize := l.Size() t.Logf("lattice grew from %d to %d nodes", initialSize, finalSize) if finalSize <= initialSize { t.Error("engine should have grown the lattice via Explore/Nucleate") } }