Files
beads/internal/formula/advice_test.go
Steve Yegge a643b9ab67 Implement advice operators for formula composition (gt-8tmz.2)
Add Lisp-style advice operators to the formula DSL:
- before(target, step) - insert step before target
- after(target, step) - insert step after target
- around(target, wrapper) - wrap target with before/after

Features:
- Glob pattern matching for targets (*.implement, shiny.*, etc)
- Pointcut matching by type or label
- Step reference substitution ({step.id}, {step.title})
- Automatic dependency chaining

New types: AdviceRule, AdviceStep, AroundAdvice, Pointcut
New functions: ApplyAdvice, MatchGlob, MatchPointcut

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-25 01:53:43 -08:00

324 lines
7.4 KiB
Go

package formula
import (
"testing"
)
func TestMatchGlob(t *testing.T) {
tests := []struct {
pattern string
stepID string
want bool
}{
// Exact matches
{"design", "design", true},
{"design", "implement", false},
{"design", "design.draft", false},
// Wildcard all
{"*", "design", true},
{"*", "implement.draft", true},
{"*", "", true},
// Suffix patterns (*.suffix)
{"*.implement", "shiny.implement", true},
{"*.implement", "design.implement", true},
{"*.implement", "implement", false},
{"*.implement", "shiny.design", false},
// Prefix patterns (prefix.*)
{"shiny.*", "shiny.design", true},
{"shiny.*", "shiny.implement", true},
{"shiny.*", "shiny", false},
{"shiny.*", "enterprise.design", false},
// Complex patterns
{"*.refine-*", "implement.refine-1", true},
{"*.refine-*", "implement.refine-2", true},
{"*.refine-*", "implement.draft", false},
}
for _, tt := range tests {
t.Run(tt.pattern+"_"+tt.stepID, func(t *testing.T) {
got := MatchGlob(tt.pattern, tt.stepID)
if got != tt.want {
t.Errorf("MatchGlob(%q, %q) = %v, want %v", tt.pattern, tt.stepID, got, tt.want)
}
})
}
}
func TestApplyAdvice_Before(t *testing.T) {
steps := []*Step{
{ID: "design", Title: "Design"},
{ID: "implement", Title: "Implement"},
}
advice := []*AdviceRule{
{
Target: "implement",
Before: &AdviceStep{
ID: "lint-{step.id}",
Title: "Lint before {step.id}",
},
},
}
result := ApplyAdvice(steps, advice)
if len(result) != 3 {
t.Fatalf("expected 3 steps, got %d", len(result))
}
// Check order: design, lint-implement, implement
if result[0].ID != "design" {
t.Errorf("expected first step 'design', got %q", result[0].ID)
}
if result[1].ID != "lint-implement" {
t.Errorf("expected second step 'lint-implement', got %q", result[1].ID)
}
if result[2].ID != "implement" {
t.Errorf("expected third step 'implement', got %q", result[2].ID)
}
// Check that implement now depends on lint-implement
if !contains(result[2].Needs, "lint-implement") {
t.Errorf("implement should depend on lint-implement, got needs: %v", result[2].Needs)
}
}
func TestApplyAdvice_After(t *testing.T) {
steps := []*Step{
{ID: "implement", Title: "Implement"},
{ID: "submit", Title: "Submit"},
}
advice := []*AdviceRule{
{
Target: "implement",
After: &AdviceStep{
ID: "test-{step.id}",
Title: "Test after {step.id}",
},
},
}
result := ApplyAdvice(steps, advice)
if len(result) != 3 {
t.Fatalf("expected 3 steps, got %d", len(result))
}
// Check order: implement, test-implement, submit
if result[0].ID != "implement" {
t.Errorf("expected first step 'implement', got %q", result[0].ID)
}
if result[1].ID != "test-implement" {
t.Errorf("expected second step 'test-implement', got %q", result[1].ID)
}
if result[2].ID != "submit" {
t.Errorf("expected third step 'submit', got %q", result[2].ID)
}
// Check that test-implement depends on implement
if !contains(result[1].Needs, "implement") {
t.Errorf("test-implement should depend on implement, got needs: %v", result[1].Needs)
}
}
func TestApplyAdvice_Around(t *testing.T) {
steps := []*Step{
{ID: "implement", Title: "Implement"},
}
advice := []*AdviceRule{
{
Target: "implement",
Around: &AroundAdvice{
Before: []*AdviceStep{
{ID: "pre-scan", Title: "Pre-scan"},
},
After: []*AdviceStep{
{ID: "post-scan", Title: "Post-scan"},
},
},
},
}
result := ApplyAdvice(steps, advice)
if len(result) != 3 {
t.Fatalf("expected 3 steps, got %d", len(result))
}
// Check order: pre-scan, implement, post-scan
if result[0].ID != "pre-scan" {
t.Errorf("expected first step 'pre-scan', got %q", result[0].ID)
}
if result[1].ID != "implement" {
t.Errorf("expected second step 'implement', got %q", result[1].ID)
}
if result[2].ID != "post-scan" {
t.Errorf("expected third step 'post-scan', got %q", result[2].ID)
}
// Check dependencies
if !contains(result[1].Needs, "pre-scan") {
t.Errorf("implement should depend on pre-scan, got needs: %v", result[1].Needs)
}
if !contains(result[2].Needs, "implement") {
t.Errorf("post-scan should depend on implement, got needs: %v", result[2].Needs)
}
}
func TestApplyAdvice_GlobPattern(t *testing.T) {
steps := []*Step{
{ID: "design", Title: "Design"},
{ID: "shiny.implement", Title: "Implement"},
{ID: "shiny.review", Title: "Review"},
}
advice := []*AdviceRule{
{
Target: "shiny.*",
Before: &AdviceStep{
ID: "log-{step.id}",
Title: "Log {step.id}",
},
},
}
result := ApplyAdvice(steps, advice)
// Should have: design, log-shiny.implement, shiny.implement, log-shiny.review, shiny.review
if len(result) != 5 {
t.Fatalf("expected 5 steps, got %d", len(result))
}
if result[0].ID != "design" {
t.Errorf("expected first step 'design', got %q", result[0].ID)
}
if result[1].ID != "log-shiny.implement" {
t.Errorf("expected second step 'log-shiny.implement', got %q", result[1].ID)
}
if result[2].ID != "shiny.implement" {
t.Errorf("expected third step 'shiny.implement', got %q", result[2].ID)
}
}
func TestApplyAdvice_NoMatch(t *testing.T) {
steps := []*Step{
{ID: "design", Title: "Design"},
{ID: "implement", Title: "Implement"},
}
advice := []*AdviceRule{
{
Target: "nonexistent",
Before: &AdviceStep{
ID: "lint",
Title: "Lint",
},
},
}
result := ApplyAdvice(steps, advice)
// No changes expected
if len(result) != 2 {
t.Fatalf("expected 2 steps, got %d", len(result))
}
if result[0].ID != "design" || result[1].ID != "implement" {
t.Errorf("steps should be unchanged")
}
}
func TestApplyAdvice_EmptyAdvice(t *testing.T) {
steps := []*Step{
{ID: "design", Title: "Design"},
}
result := ApplyAdvice(steps, nil)
if len(result) != 1 || result[0].ID != "design" {
t.Errorf("empty advice should return original steps")
}
}
func TestMatchPointcut(t *testing.T) {
tests := []struct {
name string
pc *Pointcut
step *Step
want bool
}{
{
name: "glob match",
pc: &Pointcut{Glob: "*.implement"},
step: &Step{ID: "shiny.implement"},
want: true,
},
{
name: "glob no match",
pc: &Pointcut{Glob: "*.implement"},
step: &Step{ID: "shiny.design"},
want: false,
},
{
name: "type match",
pc: &Pointcut{Type: "bug"},
step: &Step{ID: "fix", Type: "bug"},
want: true,
},
{
name: "type no match",
pc: &Pointcut{Type: "bug"},
step: &Step{ID: "fix", Type: "task"},
want: false,
},
{
name: "label match",
pc: &Pointcut{Label: "security"},
step: &Step{ID: "audit", Labels: []string{"security", "review"}},
want: true,
},
{
name: "label no match",
pc: &Pointcut{Label: "security"},
step: &Step{ID: "audit", Labels: []string{"review"}},
want: false,
},
{
name: "combined match",
pc: &Pointcut{Glob: "*.implement", Type: "task"},
step: &Step{ID: "shiny.implement", Type: "task"},
want: true,
},
{
name: "combined partial fail",
pc: &Pointcut{Glob: "*.implement", Type: "task"},
step: &Step{ID: "shiny.implement", Type: "bug"},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := MatchPointcut(tt.pc, tt.step)
if got != tt.want {
t.Errorf("MatchPointcut() = %v, want %v", got, tt.want)
}
})
}
}
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}