Files
beads/internal/recipes/recipes_test.go
dave 6730fce9b1 feat(setup): refactor to recipe-based architecture (bd-i3ed)
Replace tool-specific setup commands with a generic recipe-based system.
New tools become config entries, not code changes.

Changes:
- Add internal/recipes/ package with Recipe type and built-in recipes
- Add --list flag to show available recipes
- Add --print flag to output template to stdout
- Add -o flag to write template to arbitrary path
- Add --add flag to save custom recipes to .beads/recipes.toml
- Add built-in recipes: windsurf, cody, kilocode (new)
- Legacy recipes (cursor, claude, gemini, aider, factory) continue to work

The recipe system enables:
- Adding new tool support without code changes
- User-defined recipes in .beads/recipes.toml
- Shared template across all file-based integrations

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Executed-By: beads/crew/dave
Rig: beads
Role: crew
2026-01-04 21:57:09 -08:00

189 lines
4.4 KiB
Go

package recipes
import (
"os"
"path/filepath"
"testing"
)
func TestBuiltinRecipes(t *testing.T) {
// Ensure all expected built-in recipes exist
expected := []string{"cursor", "windsurf", "cody", "kilocode", "claude", "gemini", "factory", "aider"}
for _, name := range expected {
recipe, ok := BuiltinRecipes[name]
if !ok {
t.Errorf("missing built-in recipe: %s", name)
continue
}
if recipe.Name == "" {
t.Errorf("recipe %s has empty Name", name)
}
if recipe.Type == "" {
t.Errorf("recipe %s has empty Type", name)
}
}
}
func TestGetRecipe(t *testing.T) {
// Test getting a built-in recipe
recipe, err := GetRecipe("cursor", "")
if err != nil {
t.Fatalf("GetRecipe(cursor): %v", err)
}
if recipe.Name != "Cursor IDE" {
t.Errorf("got Name=%q, want 'Cursor IDE'", recipe.Name)
}
if recipe.Path != ".cursor/rules/beads.mdc" {
t.Errorf("got Path=%q, want '.cursor/rules/beads.mdc'", recipe.Path)
}
// Test unknown recipe
_, err = GetRecipe("nonexistent", "")
if err == nil {
t.Error("GetRecipe(nonexistent) should return error")
}
}
func TestIsBuiltin(t *testing.T) {
if !IsBuiltin("cursor") {
t.Error("cursor should be builtin")
}
if IsBuiltin("myeditor") {
t.Error("myeditor should not be builtin")
}
}
func TestListRecipeNames(t *testing.T) {
names, err := ListRecipeNames("")
if err != nil {
t.Fatalf("ListRecipeNames: %v", err)
}
// Check that it's sorted
for i := 1; i < len(names); i++ {
if names[i-1] > names[i] {
t.Errorf("names not sorted: %v", names)
break
}
}
// Check expected recipes present
found := make(map[string]bool)
for _, name := range names {
found[name] = true
}
for _, expected := range []string{"cursor", "claude", "aider"} {
if !found[expected] {
t.Errorf("expected recipe %s not in list", expected)
}
}
}
func TestUserRecipes(t *testing.T) {
// Create temp directory
tmpDir, err := os.MkdirTemp("", "recipes-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
// Save a user recipe
if err := SaveUserRecipe(tmpDir, "myeditor", ".myeditor/rules.md"); err != nil {
t.Fatalf("SaveUserRecipe: %v", err)
}
// Verify file was created
recipesPath := filepath.Join(tmpDir, "recipes.toml")
if _, err := os.Stat(recipesPath); os.IsNotExist(err) {
t.Error("recipes.toml was not created")
}
// Load user recipes
userRecipes, err := LoadUserRecipes(tmpDir)
if err != nil {
t.Fatalf("LoadUserRecipes: %v", err)
}
recipe, ok := userRecipes["myeditor"]
if !ok {
t.Fatal("myeditor recipe not found")
}
if recipe.Path != ".myeditor/rules.md" {
t.Errorf("got Path=%q, want '.myeditor/rules.md'", recipe.Path)
}
if recipe.Type != TypeFile {
t.Errorf("got Type=%q, want 'file'", recipe.Type)
}
// GetRecipe should find user recipe
r, err := GetRecipe("myeditor", tmpDir)
if err != nil {
t.Fatalf("GetRecipe(myeditor): %v", err)
}
if r.Path != ".myeditor/rules.md" {
t.Errorf("got Path=%q, want '.myeditor/rules.md'", r.Path)
}
}
func TestUserRecipeOverride(t *testing.T) {
// Create temp directory
tmpDir, err := os.MkdirTemp("", "recipes-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
// Save a user recipe that overrides cursor
if err := SaveUserRecipe(tmpDir, "cursor", ".my-cursor/rules.md"); err != nil {
t.Fatalf("SaveUserRecipe: %v", err)
}
// GetRecipe should return user's version
r, err := GetRecipe("cursor", tmpDir)
if err != nil {
t.Fatalf("GetRecipe(cursor): %v", err)
}
if r.Path != ".my-cursor/rules.md" {
t.Errorf("user override failed: got Path=%q, want '.my-cursor/rules.md'", r.Path)
}
}
func TestLoadUserRecipesNoFile(t *testing.T) {
// Should return nil, nil when no file exists
recipes, err := LoadUserRecipes("/nonexistent/path")
if err != nil {
t.Errorf("LoadUserRecipes should not error on missing file: %v", err)
}
if recipes != nil {
t.Error("LoadUserRecipes should return nil for missing file")
}
}
func TestTemplate(t *testing.T) {
// Basic sanity check that template is not empty
if len(Template) < 100 {
t.Error("Template is suspiciously short")
}
// Check for key content
if !containsAll(Template, "Beads", "bd ready", "bd create", "bd close") {
t.Error("Template missing expected content")
}
}
func containsAll(s string, substrs ...string) bool {
for _, sub := range substrs {
found := false
for i := 0; i <= len(s)-len(sub); i++ {
if s[i:i+len(sub)] == sub {
found = true
break
}
}
if !found {
return false
}
}
return true
}