directive_test.go raw

   1  package organ
   2  
   3  import (
   4  	"testing"
   5  
   6  	"git.mleku.dev/mleku/dendrite/pkg/axiom"
   7  	"git.mleku.dev/mleku/dendrite/pkg/dissolve"
   8  	"git.mleku.dev/mleku/dendrite/pkg/lattice"
   9  	"git.mleku.dev/mleku/dendrite/pkg/ratio"
  10  )
  11  
  12  func TestDirectiveElementInterface(t *testing.T) {
  13  	d := NewDirectiveElement("evolve faster")
  14  
  15  	// Implements axiom.Element.
  16  	var _ axiom.Element = d
  17  	if d.Type() != "directive" {
  18  		t.Errorf("expected type 'directive', got %q", d.Type())
  19  	}
  20  	if d.Value().(string) != "evolve faster" {
  21  		t.Errorf("expected value 'evolve faster', got %q", d.Value())
  22  	}
  23  
  24  	// Implements axiom.StickyElement.
  25  	var _ axiom.StickyElement = d
  26  	if !d.IsSticky() {
  27  		t.Error("default directive should be sticky")
  28  	}
  29  
  30  	// Non-sticky directive.
  31  	d2 := DirectiveElement{Directive: Directive{Text: "test", Sticky: false}}
  32  	if d2.IsSticky() {
  33  		t.Error("non-sticky directive should not be sticky")
  34  	}
  35  }
  36  
  37  func TestDirectiveConstraint(t *testing.T) {
  38  	c := DirectiveConstraint{}
  39  
  40  	if c.Tag() != "directive" {
  41  		t.Errorf("expected tag 'directive', got %q", c.Tag())
  42  	}
  43  
  44  	// Should admit DirectiveElement.
  45  	d := NewDirectiveElement("test")
  46  	if !c.Admits(d) {
  47  		t.Error("should admit DirectiveElement")
  48  	}
  49  
  50  	// Should not admit other element types.
  51  	other := mockElement{typ: "fragment", val: "foo"}
  52  	if c.Admits(other) {
  53  		t.Error("should not admit non-directive element")
  54  	}
  55  }
  56  
  57  // mockElement implements axiom.Element for testing.
  58  type mockElement struct {
  59  	typ string
  60  	val any
  61  }
  62  
  63  func (m mockElement) Type() string { return m.typ }
  64  func (m mockElement) Value() any   { return m.val }
  65  
  66  func TestTokenizeDirective(t *testing.T) {
  67  	tokens := TokenizeDirective("learn to compile Go code", true, ratio.One)
  68  
  69  	expected := []string{"learn", "to", "compile", "Go", "code"}
  70  	if len(tokens) != len(expected) {
  71  		t.Fatalf("expected %d tokens, got %d", len(expected), len(tokens))
  72  	}
  73  
  74  	for i, tok := range tokens {
  75  		if tok.Directive.Text != expected[i] {
  76  			t.Errorf("token %d: expected %q, got %q", i, expected[i], tok.Directive.Text)
  77  		}
  78  		if !tok.IsSticky() {
  79  			t.Errorf("token %d should be sticky", i)
  80  		}
  81  		if !tok.Directive.Priority.Equal(ratio.One) {
  82  			t.Errorf("token %d priority: expected 1/1, got %s", i, tok.Directive.Priority)
  83  		}
  84  	}
  85  
  86  	// Non-sticky tokens.
  87  	tokens = TokenizeDirective("soft hint", false, ratio.Half)
  88  	if len(tokens) != 2 {
  89  		t.Fatalf("expected 2 tokens, got %d", len(tokens))
  90  	}
  91  	for _, tok := range tokens {
  92  		if tok.IsSticky() {
  93  			t.Error("should not be sticky")
  94  		}
  95  		if !tok.Directive.Priority.Equal(ratio.Half) {
  96  			t.Errorf("expected priority 1/2, got %s", tok.Directive.Priority)
  97  		}
  98  	}
  99  }
 100  
 101  func TestTokenizeDirectiveEdgeCases(t *testing.T) {
 102  	// Empty string.
 103  	if tokens := TokenizeDirective("", true, ratio.One); len(tokens) != 0 {
 104  		t.Errorf("empty string should produce 0 tokens, got %d", len(tokens))
 105  	}
 106  
 107  	// Only whitespace.
 108  	if tokens := TokenizeDirective("  \t\n  ", true, ratio.One); len(tokens) != 0 {
 109  		t.Errorf("whitespace should produce 0 tokens, got %d", len(tokens))
 110  	}
 111  
 112  	// Single word.
 113  	tokens := TokenizeDirective("optimize", true, ratio.One)
 114  	if len(tokens) != 1 || tokens[0].Directive.Text != "optimize" {
 115  		t.Errorf("single word: expected ['optimize'], got %v", tokens)
 116  	}
 117  
 118  	// Multiple spaces.
 119  	tokens = TokenizeDirective("a   b   c", true, ratio.One)
 120  	if len(tokens) != 3 {
 121  		t.Errorf("multiple spaces: expected 3 tokens, got %d", len(tokens))
 122  	}
 123  }
 124  
 125  func TestStickyDirectiveSurvivesDissolution(t *testing.T) {
 126  	l := lattice.New()
 127  
 128  	// Create a directive site with one constraint.
 129  	dirNode := l.AddNode([]axiom.Constraint{DirectiveConstraint{}})
 130  
 131  	// Bond a sticky directive.
 132  	d := NewDirectiveElement("persist")
 133  	if !dirNode.Bond(d) {
 134  		t.Fatal("directive should bond")
 135  	}
 136  
 137  	// Create a regular site with a weak constraint.
 138  	regNode := l.AddNode([]axiom.Constraint{mockConstraint{tag: "text", admitAll: true}})
 139  	regular := mockElement{typ: "text", val: "ephemeral"}
 140  	if !regNode.Bond(regular) {
 141  		t.Fatal("regular element should bond")
 142  	}
 143  
 144  	// Both nodes occupied.
 145  	if !dirNode.Occupied() || !regNode.Occupied() {
 146  		t.Fatal("both should be occupied")
 147  	}
 148  
 149  	// Run dissolution with a high threshold that would dissolve everything.
 150  	dissolved := make(chan axiom.Element, 10)
 151  	events := make(chan dissolve.Event, 10)
 152  	dissolve.ScanOnce(l, dissolve.Config{Threshold: ratio.FromInt(999)}, dissolved, events)
 153  
 154  	// The sticky directive should survive.
 155  	if !dirNode.Occupied() {
 156  		t.Error("sticky directive should survive dissolution")
 157  	}
 158  
 159  	// The regular element should be dissolved.
 160  	if regNode.Occupied() {
 161  		t.Error("regular element should be dissolved")
 162  	}
 163  
 164  	// One element should have been dissolved.
 165  	select {
 166  	case elem := <-dissolved:
 167  		if elem.Type() != "text" {
 168  			t.Errorf("expected dissolved element type 'text', got %q", elem.Type())
 169  		}
 170  	default:
 171  		t.Error("expected one dissolved element")
 172  	}
 173  }
 174  
 175  func TestNonStickyDirectiveDissolvable(t *testing.T) {
 176  	l := lattice.New()
 177  
 178  	dirNode := l.AddNode([]axiom.Constraint{DirectiveConstraint{}})
 179  
 180  	// Bond a non-sticky directive.
 181  	d := DirectiveElement{Directive: Directive{Text: "soft hint", Sticky: false}}
 182  	if !dirNode.Bond(d) {
 183  		t.Fatal("directive should bond")
 184  	}
 185  
 186  	// High threshold — should dissolve non-sticky.
 187  	dissolved := make(chan axiom.Element, 10)
 188  	events := make(chan dissolve.Event, 10)
 189  	dissolve.ScanOnce(l, dissolve.Config{Threshold: ratio.FromInt(999)}, dissolved, events)
 190  
 191  	if dirNode.Occupied() {
 192  		t.Error("non-sticky directive should be dissolved")
 193  	}
 194  }
 195  
 196  // mockConstraint is a simple constraint for testing.
 197  type mockConstraint struct {
 198  	tag      string
 199  	admitAll bool
 200  }
 201  
 202  func (c mockConstraint) Tag() string              { return c.tag }
 203  func (c mockConstraint) Admits(e axiom.Element) bool { return c.admitAll }
 204