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 } // TestApplyAdvice_SelfMatchingPrevention verifies that advice doesn't match // steps it inserted (gt-8tmz.16). func TestApplyAdvice_SelfMatchingPrevention(t *testing.T) { // An advice rule with a broad pattern that would match its own insertions // if self-matching weren't prevented. advice := []*AdviceRule{ { Target: "*", // Matches everything Around: &AroundAdvice{ Before: []*AdviceStep{ {ID: "{step.id}-before", Title: "Before {step.id}"}, }, After: []*AdviceStep{ {ID: "{step.id}-after", Title: "After {step.id}"}, }, }, }, } steps := []*Step{ {ID: "implement", Title: "Implement"}, } result := ApplyAdvice(steps, advice) // Without self-matching prevention, pattern "*" would match the inserted // "implement-before" and "implement-after" steps, causing them to also // get before/after steps, leading to potential infinite expansion. // With prevention, we should only get the original step + its advice. // Expected: implement-before, implement, implement-after (3 steps) if len(result) != 3 { t.Errorf("ApplyAdvice() produced %d steps, want 3. Got IDs: %v", len(result), getStepIDs(result)) } expectedIDs := []string{"implement-before", "implement", "implement-after"} for i, want := range expectedIDs { if result[i].ID != want { t.Errorf("result[%d].ID = %q, want %q", i, result[i].ID, want) } } } func getStepIDs(steps []*Step) []string { ids := make([]string, len(steps)) for i, s := range steps { ids[i] = s.ID } return ids }