package causal import ( "testing" "git.mleku.dev/mleku/dendrite/pkg/axiom" ) type elem struct { tag string val string } func (e elem) Type() string { return e.tag } func (e elem) Value() any { return e.val } func TestExtractDefines(t *testing.T) { tests := []struct { src string want []string }{ {"x := 1", []string{"x"}}, {"x, y := foo()", []string{"x", "y"}}, {"var z int", []string{"z"}}, {"fmt.Println(x)", nil}, {"return x + 1", nil}, } for _, tt := range tests { defs := ExtractDefines(tt.src) for _, w := range tt.want { if !defs[w] { t.Errorf("ExtractDefines(%q) missing %q", tt.src, w) } } if tt.want == nil && len(defs) > 0 { t.Errorf("ExtractDefines(%q) = %v, want empty", tt.src, defs) } } } func TestExtractUses(t *testing.T) { tests := []struct { src string want []string not []string }{ {"x := foo()", []string{"foo"}, []string{"x"}}, {"fmt.Println(x)", []string{"fmt", "Println", "x"}, nil}, {"return x + 1", []string{"x"}, nil}, {"y := x + 1", []string{"x"}, []string{"y"}}, } for _, tt := range tests { uses := ExtractUses(tt.src) for _, w := range tt.want { if !uses[w] { t.Errorf("ExtractUses(%q) missing %q", tt.src, w) } } for _, n := range tt.not { if uses[n] { t.Errorf("ExtractUses(%q) should not contain %q", tt.src, n) } } } } func TestCausalConstraintAdmitsInContext(t *testing.T) { cc := CausalConstraint{ConstraintTag: "assign"} // Element that defines x. defX := elem{"assign", "main\x00x := 1"} // Element that uses x. useX := elem{"assign", "main\x00y := x + 1"} // Element with no dependencies. selfContained := elem{"assign", "main\x00a := 42"} // With defX as neighbor, useX should be admitted. neighbors := []axiom.Element{defX} if !cc.AdmitsInContext(useX, neighbors) { t.Error("should admit element when neighbor defines used identifier") } // Self-contained element should always be admitted. if !cc.AdmitsInContext(selfContained, nil) { t.Error("should admit self-contained element with no neighbors") } // Even with no matching neighbors, should still admit (lenient). noMatchNeighbors := []axiom.Element{elem{"assign", "main\x00z := 99"}} if !cc.AdmitsInContext(useX, noMatchNeighbors) { t.Error("should admit element even without matching neighbor (lenient)") } // Wrong type should be rejected. wrongType := elem{"return", "main\x00return x"} if cc.AdmitsInContext(wrongType, neighbors) { t.Error("should reject element with wrong type tag") } } func TestScopeConstraint(t *testing.T) { sc := ScopeConstraint{ConstraintTag: "assign"} defX := elem{"assign", "main\x00x := 1"} useX := elem{"assign", "main\x00y := x + 1"} selfContained := elem{"assign", "main\x00a := 42"} pureConsumer := elem{"expr", "main\x00fmt.Println(x)"} // With defining neighbor, use should be admitted. if !sc.AdmitsInContext(useX, []axiom.Element{defX}) { t.Error("should admit when same-scope neighbor defines used identifier") } // Self-contained element (defines something) always admitted. if !sc.AdmitsInContext(selfContained, []axiom.Element{defX}) { t.Error("should admit self-contained element") } // No neighbors — seed bonding, always admitted. if !sc.AdmitsInContext(useX, nil) { t.Error("should admit with no neighbors (seed bonding)") } // Pure consumer with no matching definitions — rejected by ScopeConstraint. scExpr := ScopeConstraint{ConstraintTag: "expr"} unrelated := elem{"assign", "main\x00z := 99"} if scExpr.AdmitsInContext(pureConsumer, []axiom.Element{unrelated}) { t.Error("should reject pure consumer when no neighbor defines used identifiers") } // Different parent scope — should not count as scope neighbor. diffScope := elem{"assign", "other\x00x := 1"} if !sc.AdmitsInContext(useX, []axiom.Element{diffScope}) { t.Error("should admit when only neighbors are from different scope (treated as no scope neighbor)") } } func TestUniqueDefConstraint(t *testing.T) { uc := UniqueDefConstraint{ConstraintTag: "assign"} defX := elem{"assign", "main\x00x := 1"} defXAgain := elem{"assign", "main\x00x := 2"} defY := elem{"assign", "main\x00y := 3"} // No conflict — different identifiers. if !uc.AdmitsInContext(defY, []axiom.Element{defX}) { t.Error("should admit when no duplicate definitions") } // Conflict — same identifier in same scope. if uc.AdmitsInContext(defXAgain, []axiom.Element{defX}) { t.Error("should reject duplicate definition of same identifier in same scope") } // Different scope — no conflict. defXOther := elem{"assign", "other\x00x := 1"} if !uc.AdmitsInContext(defXAgain, []axiom.Element{defXOther}) { t.Error("should admit when duplicate is in different scope") } // No definitions — always admitted. nodef := elem{"expr", "main\x00fmt.Println(x)"} ucExpr := UniqueDefConstraint{ConstraintTag: "expr"} if !ucExpr.AdmitsInContext(nodef, []axiom.Element{defX}) { t.Error("should admit element with no definitions") } // No neighbors — always admitted. if !uc.AdmitsInContext(defX, nil) { t.Error("should admit with no neighbors") } }