From 0d6b3d7e854a0e91cfee3f11f8970cbaec4fc2a1 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Thu, 18 Dec 2025 00:18:37 -0800 Subject: [PATCH] refactor: Remove YAML template system entirely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the legacy YAML-based simple template system that provided bug/epic/feature templates for --from-template flag on bd create. Removed: - cmd/bd/templates/bug.yaml, epic.yaml, feature.yaml - Template struct and all YAML loading functions in template.go - --from-template flag from bd create command - template_security_test.go (tested removed functions) - YAML template tests from template_test.go The Beads template system remains (epics with "template" label, instantiated via bd template instantiate with variable substitution). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/create.go | 37 +-- cmd/bd/template.go | 399 +++++-------------------------- cmd/bd/template_security_test.go | 44 ---- cmd/bd/template_test.go | 182 -------------- cmd/bd/templates/bug.yaml | 56 ----- cmd/bd/templates/epic.yaml | 51 ---- cmd/bd/templates/feature.yaml | 60 ----- 7 files changed, 59 insertions(+), 770 deletions(-) delete mode 100644 cmd/bd/template_security_test.go delete mode 100644 cmd/bd/templates/bug.yaml delete mode 100644 cmd/bd/templates/epic.yaml delete mode 100644 cmd/bd/templates/feature.yaml diff --git a/cmd/bd/create.go b/cmd/bd/create.go index 3042fbd1..353a4944 100644 --- a/cmd/bd/create.go +++ b/cmd/bd/create.go @@ -25,7 +25,6 @@ var createCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { CheckReadonly("create") file, _ := cmd.Flags().GetString("file") - fromTemplate, _ := cmd.Flags().GetString("from-template") // If file flag is provided, parse markdown and create multiple issues if file != "" { @@ -65,21 +64,8 @@ var createCmd = &cobra.Command{ fmt.Fprintf(os.Stderr, " For testing, consider using: BEADS_DB=/tmp/test.db ./bd create \"Test issue\"\n") } - // Load template if specified - var tmpl *Template - if fromTemplate != "" { - var err error - tmpl, err = loadTemplate(fromTemplate) - if err != nil { - FatalError("%v", err) - } - } - - // Get field values, preferring explicit flags over template defaults + // Get field values description, _ := getDescriptionFlag(cmd) - if description == "" && tmpl != nil { - description = tmpl.Description - } // Warn if creating an issue without a description (unless it's a test issue or silent mode) if description == "" && !strings.Contains(strings.ToLower(title), "test") && !silent && !debug.IsQuiet() { @@ -90,31 +76,16 @@ var createCmd = &cobra.Command{ } design, _ := cmd.Flags().GetString("design") - if design == "" && tmpl != nil { - design = tmpl.Design - } - acceptance, _ := cmd.Flags().GetString("acceptance") - if acceptance == "" && tmpl != nil { - acceptance = tmpl.AcceptanceCriteria - } - + // Parse priority (supports both "1" and "P1" formats) priorityStr, _ := cmd.Flags().GetString("priority") priority, err := validation.ValidatePriority(priorityStr) if err != nil { FatalError("%v", err) } - if cmd.Flags().Changed("priority") == false && tmpl != nil { - priority = tmpl.Priority - } issueType, _ := cmd.Flags().GetString("type") - if !cmd.Flags().Changed("type") && tmpl != nil && tmpl.Type != "" { - // Flag not explicitly set and template has a type, use template - issueType = tmpl.Type - } - assignee, _ := cmd.Flags().GetString("assignee") labels, _ := cmd.Flags().GetStringSlice("labels") @@ -122,9 +93,6 @@ var createCmd = &cobra.Command{ if len(labelAlias) > 0 { labels = append(labels, labelAlias...) } - if len(labels) == 0 && tmpl != nil && len(tmpl.Labels) > 0 { - labels = tmpl.Labels - } explicitID, _ := cmd.Flags().GetString("id") parentID, _ := cmd.Flags().GetString("parent") @@ -421,7 +389,6 @@ var createCmd = &cobra.Command{ func init() { createCmd.Flags().StringP("file", "f", "", "Create multiple issues from markdown file") - createCmd.Flags().String("from-template", "", "Create issue from template (e.g., 'epic', 'bug', 'feature')") createCmd.Flags().String("title", "", "Issue title (alternative to positional argument)") createCmd.Flags().Bool("silent", false, "Output only the issue ID (for scripting)") registerPriorityFlag(createCmd, "2") diff --git a/cmd/bd/template.go b/cmd/bd/template.go index bb831ee6..057fe9ae 100644 --- a/cmd/bd/template.go +++ b/cmd/bd/template.go @@ -2,11 +2,9 @@ package main import ( "context" - "embed" "encoding/json" "fmt" "os" - "path/filepath" "regexp" "strings" "time" @@ -17,23 +15,8 @@ import ( "github.com/steveyegge/beads/internal/storage" "github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/utils" - "gopkg.in/yaml.v3" ) -//go:embed templates/*.yaml -var builtinTemplates embed.FS - -// Template represents a simple YAML issue template (for --from-template) -type Template struct { - Name string `yaml:"name" json:"name"` - Description string `yaml:"description" json:"description"` - Type string `yaml:"type" json:"type"` - Priority int `yaml:"priority" json:"priority"` - Labels []string `yaml:"labels" json:"labels"` - Design string `yaml:"design" json:"design"` - AcceptanceCriteria string `yaml:"acceptance_criteria" json:"acceptance_criteria"` -} - // BeadsTemplateLabel is the label used to identify Beads-based templates const BeadsTemplateLabel = "template" @@ -58,166 +41,100 @@ type InstantiateResult struct { var templateCmd = &cobra.Command{ Use: "template", Short: "Manage issue templates", - Long: `Manage issue templates for streamlined issue creation. + Long: `Manage Beads templates for creating issue hierarchies. -There are two types of templates: +Templates are epics with the "template" label. They can have child issues +with {{variable}} placeholders that get substituted during instantiation. -1. YAML Templates (for single issues): - - Built-in: epic, bug, feature - - Custom: stored in .beads/templates/ - - Used with: bd create --from-template= +To create a template: + 1. Create an epic with child issues + 2. Add the 'template' label: bd label add template + 3. Use {{variable}} placeholders in titles/descriptions -2. Beads Templates (for issue hierarchies): - - Any epic with the "template" label - - Can have child issues with {{variable}} placeholders - - Used with: bd template instantiate --var key=value`, +To use a template: + bd template instantiate --var key=value`, } var templateListCmd = &cobra.Command{ Use: "list", Short: "List available templates", Run: func(cmd *cobra.Command, args []string) { - yamlOnly, _ := cmd.Flags().GetBool("yaml-only") - beadsOnly, _ := cmd.Flags().GetBool("beads-only") + ctx := rootCtx + var beadsTemplates []*types.Issue - type combinedOutput struct { - YAMLTemplates []Template `json:"yaml_templates,omitempty"` - BeadsTemplates []*types.Issue `json:"beads_templates,omitempty"` - } - output := combinedOutput{} - - // Load YAML templates - if !beadsOnly { - templates, err := loadAllTemplates() + if daemonClient != nil { + resp, err := daemonClient.List(&rpc.ListArgs{}) if err != nil { - fmt.Fprintf(os.Stderr, "Warning: error loading YAML templates: %v\n", err) - } else { - output.YAMLTemplates = templates + fmt.Fprintf(os.Stderr, "Error loading templates: %v\n", err) + os.Exit(1) } - } - - // Load Beads templates - if !yamlOnly { - ctx := rootCtx - var beadsTemplates []*types.Issue - var err error - - if daemonClient != nil { - resp, err := daemonClient.List(&rpc.ListArgs{}) - if err != nil { - fmt.Fprintf(os.Stderr, "Warning: error loading Beads templates: %v\n", err) - } else { - var allIssues []*types.Issue - if err := json.Unmarshal(resp.Data, &allIssues); err == nil { - for _, issue := range allIssues { - for _, label := range issue.Labels { - if label == BeadsTemplateLabel { - beadsTemplates = append(beadsTemplates, issue) - break - } - } + var allIssues []*types.Issue + if err := json.Unmarshal(resp.Data, &allIssues); err == nil { + for _, issue := range allIssues { + for _, label := range issue.Labels { + if label == BeadsTemplateLabel { + beadsTemplates = append(beadsTemplates, issue) + break } } } - } else if store != nil { - beadsTemplates, err = store.GetIssuesByLabel(ctx, BeadsTemplateLabel) - if err != nil { - fmt.Fprintf(os.Stderr, "Warning: error loading Beads templates: %v\n", err) - } } - output.BeadsTemplates = beadsTemplates + } else if store != nil { + var err error + beadsTemplates, err = store.GetIssuesByLabel(ctx, BeadsTemplateLabel) + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading templates: %v\n", err) + os.Exit(1) + } + } else { + fmt.Fprintf(os.Stderr, "Error: no database connection\n") + os.Exit(1) } if jsonOutput { - outputJSON(output) + outputJSON(beadsTemplates) return } // Human-readable output - green := color.New(color.FgGreen).SprintFunc() - blue := color.New(color.FgBlue).SprintFunc() - cyan := color.New(color.FgCyan).SprintFunc() - - // Show YAML templates - if !beadsOnly && len(output.YAMLTemplates) > 0 { - // Group by source - builtins := []Template{} - customs := []Template{} - for _, tmpl := range output.YAMLTemplates { - if isBuiltinTemplate(tmpl.Name) { - builtins = append(builtins, tmpl) - } else { - customs = append(customs, tmpl) - } - } - - if len(builtins) > 0 { - fmt.Printf("%s\n", green("Built-in Templates (for --from-template):")) - for _, tmpl := range builtins { - fmt.Printf(" %s\n", blue(tmpl.Name)) - fmt.Printf(" Type: %s, Priority: P%d\n", tmpl.Type, tmpl.Priority) - } - fmt.Println() - } - - if len(customs) > 0 { - fmt.Printf("%s\n", green("Custom Templates (for --from-template):")) - for _, tmpl := range customs { - fmt.Printf(" %s\n", blue(tmpl.Name)) - fmt.Printf(" Type: %s, Priority: P%d\n", tmpl.Type, tmpl.Priority) - } - fmt.Println() - } - } - - // Show Beads templates - if !yamlOnly && len(output.BeadsTemplates) > 0 { - fmt.Printf("%s\n", green("Beads Templates (for bd template instantiate):")) - for _, tmpl := range output.BeadsTemplates { - vars := extractVariables(tmpl.Title + " " + tmpl.Description) - varStr := "" - if len(vars) > 0 { - varStr = fmt.Sprintf(" (vars: %s)", strings.Join(vars, ", ")) - } - fmt.Printf(" %s: %s%s\n", cyan(tmpl.ID), tmpl.Title, varStr) - } - fmt.Println() - } - - if len(output.YAMLTemplates) == 0 && len(output.BeadsTemplates) == 0 { + if len(beadsTemplates) == 0 { fmt.Println("No templates available.") - fmt.Println("\nTo create a Beads template:") + fmt.Println("\nTo create a template:") fmt.Println(" 1. Create an epic with child issues") fmt.Println(" 2. Add the 'template' label: bd label add template") fmt.Println(" 3. Use {{variable}} placeholders in titles/descriptions") + return } + + green := color.New(color.FgGreen).SprintFunc() + cyan := color.New(color.FgCyan).SprintFunc() + + fmt.Printf("%s\n", green("Templates (for bd template instantiate):")) + for _, tmpl := range beadsTemplates { + vars := extractVariables(tmpl.Title + " " + tmpl.Description) + varStr := "" + if len(vars) > 0 { + varStr = fmt.Sprintf(" (vars: %s)", strings.Join(vars, ", ")) + } + fmt.Printf(" %s: %s%s\n", cyan(tmpl.ID), tmpl.Title, varStr) + } + fmt.Println() }, } var templateShowCmd = &cobra.Command{ - Use: "show ", + Use: "show ", Short: "Show template details", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - arg := args[0] - - // Try loading as YAML template first - yamlTmpl, yamlErr := loadTemplate(arg) - if yamlErr == nil { - showYAMLTemplate(yamlTmpl) - return - } - - // Try loading as Beads template ctx := rootCtx var templateID string if daemonClient != nil { - resolveArgs := &rpc.ResolveIDArgs{ID: arg} + resolveArgs := &rpc.ResolveIDArgs{ID: args[0]} resp, err := daemonClient.ResolveID(resolveArgs) if err != nil { - // Neither YAML nor Beads template found - fmt.Fprintf(os.Stderr, "Error: template '%s' not found\n", arg) + fmt.Fprintf(os.Stderr, "Error: template '%s' not found\n", args[0]) os.Exit(1) } if err := json.Unmarshal(resp.Data, &templateID); err != nil { @@ -226,9 +143,9 @@ var templateShowCmd = &cobra.Command{ } } else if store != nil { var err error - templateID, err = utils.ResolvePartialID(ctx, store, arg) + templateID, err = utils.ResolvePartialID(ctx, store, args[0]) if err != nil { - fmt.Fprintf(os.Stderr, "Error: template '%s' not found\n", arg) + fmt.Fprintf(os.Stderr, "Error: template '%s' not found\n", args[0]) os.Exit(1) } } else { @@ -247,30 +164,6 @@ var templateShowCmd = &cobra.Command{ }, } -func showYAMLTemplate(tmpl *Template) { - if jsonOutput { - outputJSON(tmpl) - return - } - - green := color.New(color.FgGreen).SprintFunc() - blue := color.New(color.FgBlue).SprintFunc() - - fmt.Printf("%s %s (YAML template)\n", green("Template:"), blue(tmpl.Name)) - fmt.Printf("Type: %s\n", tmpl.Type) - fmt.Printf("Priority: P%d\n", tmpl.Priority) - if len(tmpl.Labels) > 0 { - fmt.Printf("Labels: %s\n", strings.Join(tmpl.Labels, ", ")) - } - fmt.Printf("\n%s\n%s\n", green("Description:"), tmpl.Description) - if tmpl.Design != "" { - fmt.Printf("\n%s\n%s\n", green("Design:"), tmpl.Design) - } - if tmpl.AcceptanceCriteria != "" { - fmt.Printf("\n%s\n%s\n", green("Acceptance Criteria:"), tmpl.AcceptanceCriteria) - } -} - func showBeadsTemplate(subgraph *TemplateSubgraph) { if jsonOutput { outputJSON(map[string]interface{}{ @@ -286,7 +179,7 @@ func showBeadsTemplate(subgraph *TemplateSubgraph) { yellow := color.New(color.FgYellow).SprintFunc() green := color.New(color.FgGreen).SprintFunc() - fmt.Printf("\n%s Template: %s (Beads template)\n", cyan("📋"), subgraph.Root.Title) + fmt.Printf("\n%s Template: %s\n", cyan("📋"), subgraph.Root.Title) fmt.Printf(" ID: %s\n", subgraph.Root.ID) fmt.Printf(" Issues: %d\n", len(subgraph.Issues)) @@ -305,67 +198,6 @@ func showBeadsTemplate(subgraph *TemplateSubgraph) { fmt.Println() } -var templateCreateCmd = &cobra.Command{ - Use: "create ", - Short: "Create a custom YAML template", - Long: `Create a custom YAML template in .beads/templates/ directory. - -This creates a simple template for pre-filling issue fields. -For workflow templates with hierarchies, create an epic and add the 'template' label.`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - templateName := args[0] - - // Sanitize template name - if err := sanitizeTemplateName(templateName); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } - - // Ensure .beads/templates directory exists - templatesDir := filepath.Join(".beads", "templates") - if err := os.MkdirAll(templatesDir, 0755); err != nil { - fmt.Fprintf(os.Stderr, "Error creating templates directory: %v\n", err) - os.Exit(1) - } - - // Create template file - templatePath := filepath.Join(templatesDir, templateName+".yaml") - if _, err := os.Stat(templatePath); err == nil { - fmt.Fprintf(os.Stderr, "Error: template '%s' already exists\n", templateName) - os.Exit(1) - } - - // Default template structure - tmpl := Template{ - Name: templateName, - Description: "[Describe the issue]\n\n## Additional Context\n\n[Add relevant details]", - Type: "task", - Priority: 2, - Labels: []string{}, - Design: "[Design notes]", - AcceptanceCriteria: "- [ ] Acceptance criterion 1\n- [ ] Acceptance criterion 2", - } - - // Marshal to YAML - data, err := yaml.Marshal(tmpl) - if err != nil { - fmt.Fprintf(os.Stderr, "Error creating template: %v\n", err) - os.Exit(1) - } - - // Write template file - if err := os.WriteFile(templatePath, data, 0600); err != nil { - fmt.Fprintf(os.Stderr, "Error writing template: %v\n", err) - os.Exit(1) - } - - green := color.New(color.FgGreen).SprintFunc() - fmt.Printf("%s Created template: %s\n", green("✓"), templatePath) - fmt.Printf("Edit the file to customize your template.\n") - }, -} - var templateInstantiateCmd = &cobra.Command{ Use: "instantiate ", Short: "Create issues from a Beads template", @@ -484,135 +316,18 @@ Example: } func init() { - templateListCmd.Flags().Bool("yaml-only", false, "Show only YAML templates") - templateListCmd.Flags().Bool("beads-only", false, "Show only Beads templates") - templateInstantiateCmd.Flags().StringSlice("var", []string{}, "Variable substitution (key=value)") templateInstantiateCmd.Flags().Bool("dry-run", false, "Preview what would be created") templateInstantiateCmd.Flags().String("assignee", "", "Assign the root epic to this agent/user") templateCmd.AddCommand(templateListCmd) templateCmd.AddCommand(templateShowCmd) - templateCmd.AddCommand(templateCreateCmd) templateCmd.AddCommand(templateInstantiateCmd) rootCmd.AddCommand(templateCmd) } // ============================================================================= -// YAML Template Functions (for --from-template) -// ============================================================================= - -// loadAllTemplates loads both built-in and custom YAML templates -func loadAllTemplates() ([]Template, error) { - templates := []Template{} - - // Load built-in templates - builtins := []string{"epic", "bug", "feature"} - for _, name := range builtins { - tmpl, err := loadBuiltinTemplate(name) - if err != nil { - continue - } - templates = append(templates, *tmpl) - } - - // Load custom templates from .beads/templates/ - templatesDir := filepath.Join(".beads", "templates") - if _, err := os.Stat(templatesDir); err == nil { - entries, err := os.ReadDir(templatesDir) - if err != nil { - return nil, fmt.Errorf("reading templates directory: %w", err) - } - - for _, entry := range entries { - if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") { - continue - } - - name := strings.TrimSuffix(entry.Name(), ".yaml") - tmpl, err := loadCustomTemplate(name) - if err != nil { - continue - } - templates = append(templates, *tmpl) - } - } - - return templates, nil -} - -// sanitizeTemplateName validates template name to prevent path traversal -func sanitizeTemplateName(name string) error { - if name != filepath.Base(name) { - return fmt.Errorf("invalid template name '%s' (no path separators allowed)", name) - } - if strings.Contains(name, "..") { - return fmt.Errorf("invalid template name '%s' (no .. allowed)", name) - } - return nil -} - -// loadTemplate loads a YAML template by name (checks custom first, then built-in) -func loadTemplate(name string) (*Template, error) { - if err := sanitizeTemplateName(name); err != nil { - return nil, err - } - - // Try custom templates first - tmpl, err := loadCustomTemplate(name) - if err == nil { - return tmpl, nil - } - - // Fall back to built-in templates - return loadBuiltinTemplate(name) -} - -// loadBuiltinTemplate loads a built-in YAML template -func loadBuiltinTemplate(name string) (*Template, error) { - path := fmt.Sprintf("templates/%s.yaml", name) - data, err := builtinTemplates.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("template '%s' not found", name) - } - - var tmpl Template - if err := yaml.Unmarshal(data, &tmpl); err != nil { - return nil, fmt.Errorf("parsing template: %w", err) - } - - return &tmpl, nil -} - -// loadCustomTemplate loads a custom YAML template from .beads/templates/ -func loadCustomTemplate(name string) (*Template, error) { - path := filepath.Join(".beads", "templates", name+".yaml") - // #nosec G304 - path is sanitized via sanitizeTemplateName before calling this function - data, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("template '%s' not found", name) - } - - var tmpl Template - if err := yaml.Unmarshal(data, &tmpl); err != nil { - return nil, fmt.Errorf("parsing template: %w", err) - } - - return &tmpl, nil -} - -// isBuiltinTemplate checks if a template name is a built-in template -func isBuiltinTemplate(name string) bool { - builtins := map[string]bool{ - "epic": true, - "bug": true, - "feature": true, - } - return builtins[name] -} - -// ============================================================================= -// Beads Template Functions (for bd template instantiate) +// Beads Template Functions // ============================================================================= // loadTemplateSubgraph loads a template epic and all its descendants diff --git a/cmd/bd/template_security_test.go b/cmd/bd/template_security_test.go deleted file mode 100644 index 241c9aad..00000000 --- a/cmd/bd/template_security_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "testing" -) - -func TestSanitizeTemplateName(t *testing.T) { - tests := []struct { - name string - input string - wantError bool - }{ - {"valid simple name", "epic", false}, - {"valid with dash", "my-template", false}, - {"valid with underscore", "my_template", false}, - {"path traversal with ../", "../etc/passwd", true}, - {"path traversal with ..", "..", true}, - {"absolute path", "/etc/passwd", true}, - {"relative path", "foo/bar", true}, - {"hidden file", ".hidden", false}, // Hidden files are okay - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := sanitizeTemplateName(tt.input) - if (err != nil) != tt.wantError { - t.Errorf("sanitizeTemplateName(%q) error = %v, wantError %v", tt.input, err, tt.wantError) - } - }) - } -} - -func TestLoadTemplatePathTraversal(t *testing.T) { - // Try to load a template with path traversal - _, err := loadTemplate("../../../etc/passwd") - if err == nil { - t.Error("Expected error for path traversal, got nil") - } - - _, err = loadTemplate("foo/bar") - if err == nil { - t.Error("Expected error for path with separator, got nil") - } -} diff --git a/cmd/bd/template_test.go b/cmd/bd/template_test.go index c8180482..d984c3ab 100644 --- a/cmd/bd/template_test.go +++ b/cmd/bd/template_test.go @@ -10,188 +10,6 @@ import ( "github.com/steveyegge/beads/internal/types" ) -func TestLoadBuiltinTemplate(t *testing.T) { - tests := []struct { - name string - templateName string - wantType string - wantPriority int - wantHasLabels bool - }{ - { - name: "epic template", - templateName: "epic", - wantType: "epic", - wantPriority: 1, - wantHasLabels: true, - }, - { - name: "bug template", - templateName: "bug", - wantType: "bug", - wantPriority: 1, - wantHasLabels: true, - }, - { - name: "feature template", - templateName: "feature", - wantType: "feature", - wantPriority: 2, - wantHasLabels: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tmpl, err := loadBuiltinTemplate(tt.templateName) - if err != nil { - t.Fatalf("loadBuiltinTemplate() error = %v", err) - } - - if tmpl.Type != tt.wantType { - t.Errorf("Type = %v, want %v", tmpl.Type, tt.wantType) - } - - if tmpl.Priority != tt.wantPriority { - t.Errorf("Priority = %v, want %v", tmpl.Priority, tt.wantPriority) - } - - if tt.wantHasLabels && len(tmpl.Labels) == 0 { - t.Errorf("Expected labels but got none") - } - - if tmpl.Description == "" { - t.Errorf("Expected description but got empty string") - } - - if tmpl.AcceptanceCriteria == "" { - t.Errorf("Expected acceptance criteria but got empty string") - } - }) - } -} - -func TestLoadBuiltinTemplateNotFound(t *testing.T) { - _, err := loadBuiltinTemplate("nonexistent") - if err == nil { - t.Errorf("Expected error for nonexistent template, got nil") - } -} - -func TestLoadCustomTemplate(t *testing.T) { - // Create temporary directory for test - tmpDir := t.TempDir() - t.Chdir(tmpDir) - - // Create .beads/templates directory - templatesDir := filepath.Join(".beads", "templates") - if err := os.MkdirAll(templatesDir, 0755); err != nil { - t.Fatalf("Failed to create templates directory: %v", err) - } - - // Create a custom template - customTemplate := `name: custom-test -description: Test custom template -type: chore -priority: 3 -labels: - - test - - custom -design: Test design -acceptance_criteria: Test acceptance -` - templatePath := filepath.Join(templatesDir, "custom-test.yaml") - if err := os.WriteFile(templatePath, []byte(customTemplate), 0644); err != nil { - t.Fatalf("Failed to write template: %v", err) - } - - // Load the custom template - tmpl, err := loadCustomTemplate("custom-test") - if err != nil { - t.Fatalf("loadCustomTemplate() error = %v", err) - } - - if tmpl.Name != "custom-test" { - t.Errorf("Name = %v, want custom-test", tmpl.Name) - } - - if tmpl.Type != "chore" { - t.Errorf("Type = %v, want chore", tmpl.Type) - } - - if tmpl.Priority != 3 { - t.Errorf("Priority = %v, want 3", tmpl.Priority) - } - - if len(tmpl.Labels) != 2 { - t.Errorf("Expected 2 labels, got %d", len(tmpl.Labels)) - } -} - -func TestLoadTemplate_PreferCustomOverBuiltin(t *testing.T) { - // Create temporary directory for test - tmpDir := t.TempDir() - t.Chdir(tmpDir) - - // Create .beads/templates directory - templatesDir := filepath.Join(".beads", "templates") - if err := os.MkdirAll(templatesDir, 0755); err != nil { - t.Fatalf("Failed to create templates directory: %v", err) - } - - // Create a custom template with same name as builtin - customTemplate := `name: epic -description: Custom epic override -type: epic -priority: 0 -labels: - - custom-epic -design: Custom design -acceptance_criteria: Custom acceptance -` - templatePath := filepath.Join(templatesDir, "epic.yaml") - if err := os.WriteFile(templatePath, []byte(customTemplate), 0644); err != nil { - t.Fatalf("Failed to write template: %v", err) - } - - // loadTemplate should prefer custom over builtin - tmpl, err := loadTemplate("epic") - if err != nil { - t.Fatalf("loadTemplate() error = %v", err) - } - - // Should get custom template (priority 0) not builtin (priority 1) - if tmpl.Priority != 0 { - t.Errorf("Priority = %v, want 0 (custom template)", tmpl.Priority) - } - - if len(tmpl.Labels) != 1 || tmpl.Labels[0] != "custom-epic" { - t.Errorf("Expected custom-epic label, got %v", tmpl.Labels) - } -} - -func TestIsBuiltinTemplate(t *testing.T) { - tests := []struct { - name string - template string - want bool - }{ - {"epic is builtin", "epic", true}, - {"bug is builtin", "bug", true}, - {"feature is builtin", "feature", true}, - {"custom is not builtin", "custom", false}, - {"random is not builtin", "random", false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := isBuiltinTemplate(tt.template); got != tt.want { - t.Errorf("isBuiltinTemplate(%v) = %v, want %v", tt.template, got, tt.want) - } - }) - } -} - // ============================================================================= // Beads Template Tests (for bd template instantiate) // ============================================================================= diff --git a/cmd/bd/templates/bug.yaml b/cmd/bd/templates/bug.yaml deleted file mode 100644 index 0e339bdb..00000000 --- a/cmd/bd/templates/bug.yaml +++ /dev/null @@ -1,56 +0,0 @@ -# Built-in template for bug reports -name: bug -description: | - ## Summary - - [Brief description of the bug] - - ## Steps to Reproduce - - 1. Step 1 - 2. Step 2 - 3. Step 3 - - ## Expected Behavior - - [What should happen] - - ## Actual Behavior - - [What actually happens] - - ## Environment - - - OS: [e.g., macOS 15.7.1] - - Version: [e.g., bd 0.20.1] - - Additional context: [any relevant details] - - ## Additional Context - - [Screenshots, logs, or other relevant information] - -type: bug -priority: 1 -labels: - - bug - -design: | - ## Root Cause Analysis - - [Describe the underlying cause once identified] - - ## Proposed Fix - - [Outline the solution approach] - - ## Impact Assessment - - - Affected features: [list] - - Breaking changes: [yes/no and details] - - Migration needed: [yes/no and details] - -acceptance_criteria: | - - [ ] Bug no longer reproduces with original steps - - [ ] Regression tests added - - [ ] Related edge cases tested - - [ ] Documentation updated if behavior changed diff --git a/cmd/bd/templates/epic.yaml b/cmd/bd/templates/epic.yaml deleted file mode 100644 index f60f7b9e..00000000 --- a/cmd/bd/templates/epic.yaml +++ /dev/null @@ -1,51 +0,0 @@ -# Built-in template for creating epics -name: epic -description: | - ## Overview - - [Describe the high-level goal and scope of this epic] - - ## Success Criteria - - - [ ] Criteria 1 - - [ ] Criteria 2 - - [ ] Criteria 3 - - ## Background - - [Provide context and motivation] - - ## Scope - - **In Scope:** - - Item 1 - - Item 2 - - **Out of Scope:** - - Item 1 - - Item 2 - -type: epic -priority: 1 -labels: - - epic - -design: | - ## Architecture - - [Describe the overall architecture and approach] - - ## Components - - - Component 1: [description] - - Component 2: [description] - - ## Dependencies - - [List external dependencies or constraints] - -acceptance_criteria: | - - [ ] All child issues are completed - - [ ] Integration tests pass - - [ ] Documentation is updated - - [ ] Code review completed diff --git a/cmd/bd/templates/feature.yaml b/cmd/bd/templates/feature.yaml deleted file mode 100644 index 09ca842b..00000000 --- a/cmd/bd/templates/feature.yaml +++ /dev/null @@ -1,60 +0,0 @@ -# Built-in template for feature requests -name: feature -description: | - ## Feature Request - - [Describe the desired feature] - - ## Motivation - - [Why is this feature needed? What problem does it solve?] - - ## Use Cases - - 1. **Use Case 1**: [description] - 2. **Use Case 2**: [description] - - ## Proposed Solution - - [High-level approach to implementing this feature] - - ## Alternatives Considered - - - **Alternative 1**: [description and why not chosen] - - **Alternative 2**: [description and why not chosen] - -type: feature -priority: 2 -labels: - - feature - -design: | - ## Technical Design - - [Detailed technical approach] - - ## API Changes - - [New commands, flags, or APIs] - - ## Data Model Changes - - [Database schema changes if any] - - ## Implementation Notes - - - Note 1 - - Note 2 - - ## Testing Strategy - - - Unit tests: [scope] - - Integration tests: [scope] - - Manual testing: [steps] - -acceptance_criteria: | - - [ ] Feature implements all described use cases - - [ ] All tests pass - - [ ] Documentation updated (README, commands) - - [ ] Examples added if applicable - - [ ] No performance regressions