refactor: Remove YAML template system entirely

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 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-18 00:18:37 -08:00
parent 0bfae2e0ab
commit 0d6b3d7e85
7 changed files with 59 additions and 770 deletions

View File

@@ -25,7 +25,6 @@ var createCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
CheckReadonly("create") CheckReadonly("create")
file, _ := cmd.Flags().GetString("file") file, _ := cmd.Flags().GetString("file")
fromTemplate, _ := cmd.Flags().GetString("from-template")
// If file flag is provided, parse markdown and create multiple issues // If file flag is provided, parse markdown and create multiple issues
if file != "" { 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") fmt.Fprintf(os.Stderr, " For testing, consider using: BEADS_DB=/tmp/test.db ./bd create \"Test issue\"\n")
} }
// Load template if specified // Get field values
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
description, _ := getDescriptionFlag(cmd) 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) // 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() { if description == "" && !strings.Contains(strings.ToLower(title), "test") && !silent && !debug.IsQuiet() {
@@ -90,31 +76,16 @@ var createCmd = &cobra.Command{
} }
design, _ := cmd.Flags().GetString("design") design, _ := cmd.Flags().GetString("design")
if design == "" && tmpl != nil {
design = tmpl.Design
}
acceptance, _ := cmd.Flags().GetString("acceptance") acceptance, _ := cmd.Flags().GetString("acceptance")
if acceptance == "" && tmpl != nil {
acceptance = tmpl.AcceptanceCriteria
}
// Parse priority (supports both "1" and "P1" formats) // Parse priority (supports both "1" and "P1" formats)
priorityStr, _ := cmd.Flags().GetString("priority") priorityStr, _ := cmd.Flags().GetString("priority")
priority, err := validation.ValidatePriority(priorityStr) priority, err := validation.ValidatePriority(priorityStr)
if err != nil { if err != nil {
FatalError("%v", err) FatalError("%v", err)
} }
if cmd.Flags().Changed("priority") == false && tmpl != nil {
priority = tmpl.Priority
}
issueType, _ := cmd.Flags().GetString("type") 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") assignee, _ := cmd.Flags().GetString("assignee")
labels, _ := cmd.Flags().GetStringSlice("labels") labels, _ := cmd.Flags().GetStringSlice("labels")
@@ -122,9 +93,6 @@ var createCmd = &cobra.Command{
if len(labelAlias) > 0 { if len(labelAlias) > 0 {
labels = append(labels, labelAlias...) labels = append(labels, labelAlias...)
} }
if len(labels) == 0 && tmpl != nil && len(tmpl.Labels) > 0 {
labels = tmpl.Labels
}
explicitID, _ := cmd.Flags().GetString("id") explicitID, _ := cmd.Flags().GetString("id")
parentID, _ := cmd.Flags().GetString("parent") parentID, _ := cmd.Flags().GetString("parent")
@@ -421,7 +389,6 @@ var createCmd = &cobra.Command{
func init() { func init() {
createCmd.Flags().StringP("file", "f", "", "Create multiple issues from markdown file") 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().String("title", "", "Issue title (alternative to positional argument)")
createCmd.Flags().Bool("silent", false, "Output only the issue ID (for scripting)") createCmd.Flags().Bool("silent", false, "Output only the issue ID (for scripting)")
registerPriorityFlag(createCmd, "2") registerPriorityFlag(createCmd, "2")

View File

@@ -2,11 +2,9 @@ package main
import ( import (
"context" "context"
"embed"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@@ -17,23 +15,8 @@ import (
"github.com/steveyegge/beads/internal/storage" "github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/utils" "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 // BeadsTemplateLabel is the label used to identify Beads-based templates
const BeadsTemplateLabel = "template" const BeadsTemplateLabel = "template"
@@ -58,166 +41,100 @@ type InstantiateResult struct {
var templateCmd = &cobra.Command{ var templateCmd = &cobra.Command{
Use: "template", Use: "template",
Short: "Manage issue templates", 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): To create a template:
- Built-in: epic, bug, feature 1. Create an epic with child issues
- Custom: stored in .beads/templates/ 2. Add the 'template' label: bd label add <epic-id> template
- Used with: bd create --from-template=<name> 3. Use {{variable}} placeholders in titles/descriptions
2. Beads Templates (for issue hierarchies): To use a template:
- Any epic with the "template" label bd template instantiate <id> --var key=value`,
- Can have child issues with {{variable}} placeholders
- Used with: bd template instantiate <id> --var key=value`,
} }
var templateListCmd = &cobra.Command{ var templateListCmd = &cobra.Command{
Use: "list", Use: "list",
Short: "List available templates", Short: "List available templates",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
yamlOnly, _ := cmd.Flags().GetBool("yaml-only") ctx := rootCtx
beadsOnly, _ := cmd.Flags().GetBool("beads-only") var beadsTemplates []*types.Issue
type combinedOutput struct { if daemonClient != nil {
YAMLTemplates []Template `json:"yaml_templates,omitempty"` resp, err := daemonClient.List(&rpc.ListArgs{})
BeadsTemplates []*types.Issue `json:"beads_templates,omitempty"`
}
output := combinedOutput{}
// Load YAML templates
if !beadsOnly {
templates, err := loadAllTemplates()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Warning: error loading YAML templates: %v\n", err) fmt.Fprintf(os.Stderr, "Error loading templates: %v\n", err)
} else { os.Exit(1)
output.YAMLTemplates = templates
} }
} var allIssues []*types.Issue
if err := json.Unmarshal(resp.Data, &allIssues); err == nil {
// Load Beads templates for _, issue := range allIssues {
if !yamlOnly { for _, label := range issue.Labels {
ctx := rootCtx if label == BeadsTemplateLabel {
var beadsTemplates []*types.Issue beadsTemplates = append(beadsTemplates, issue)
var err error break
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
}
}
} }
} }
} }
} 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 { if jsonOutput {
outputJSON(output) outputJSON(beadsTemplates)
return return
} }
// Human-readable output // Human-readable output
green := color.New(color.FgGreen).SprintFunc() if len(beadsTemplates) == 0 {
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 {
fmt.Println("No templates available.") 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(" 1. Create an epic with child issues")
fmt.Println(" 2. Add the 'template' label: bd label add <epic-id> template") fmt.Println(" 2. Add the 'template' label: bd label add <epic-id> template")
fmt.Println(" 3. Use {{variable}} placeholders in titles/descriptions") 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{ var templateShowCmd = &cobra.Command{
Use: "show <template-name-or-id>", Use: "show <template-id>",
Short: "Show template details", Short: "Show template details",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) { 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 ctx := rootCtx
var templateID string var templateID string
if daemonClient != nil { if daemonClient != nil {
resolveArgs := &rpc.ResolveIDArgs{ID: arg} resolveArgs := &rpc.ResolveIDArgs{ID: args[0]}
resp, err := daemonClient.ResolveID(resolveArgs) resp, err := daemonClient.ResolveID(resolveArgs)
if err != nil { if err != nil {
// Neither YAML nor Beads template found fmt.Fprintf(os.Stderr, "Error: template '%s' not found\n", args[0])
fmt.Fprintf(os.Stderr, "Error: template '%s' not found\n", arg)
os.Exit(1) os.Exit(1)
} }
if err := json.Unmarshal(resp.Data, &templateID); err != nil { if err := json.Unmarshal(resp.Data, &templateID); err != nil {
@@ -226,9 +143,9 @@ var templateShowCmd = &cobra.Command{
} }
} else if store != nil { } else if store != nil {
var err error var err error
templateID, err = utils.ResolvePartialID(ctx, store, arg) templateID, err = utils.ResolvePartialID(ctx, store, args[0])
if err != nil { 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) os.Exit(1)
} }
} else { } 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) { func showBeadsTemplate(subgraph *TemplateSubgraph) {
if jsonOutput { if jsonOutput {
outputJSON(map[string]interface{}{ outputJSON(map[string]interface{}{
@@ -286,7 +179,7 @@ func showBeadsTemplate(subgraph *TemplateSubgraph) {
yellow := color.New(color.FgYellow).SprintFunc() yellow := color.New(color.FgYellow).SprintFunc()
green := color.New(color.FgGreen).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(" ID: %s\n", subgraph.Root.ID)
fmt.Printf(" Issues: %d\n", len(subgraph.Issues)) fmt.Printf(" Issues: %d\n", len(subgraph.Issues))
@@ -305,67 +198,6 @@ func showBeadsTemplate(subgraph *TemplateSubgraph) {
fmt.Println() fmt.Println()
} }
var templateCreateCmd = &cobra.Command{
Use: "create <template-name>",
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{ var templateInstantiateCmd = &cobra.Command{
Use: "instantiate <template-id>", Use: "instantiate <template-id>",
Short: "Create issues from a Beads template", Short: "Create issues from a Beads template",
@@ -484,135 +316,18 @@ Example:
} }
func init() { 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().StringSlice("var", []string{}, "Variable substitution (key=value)")
templateInstantiateCmd.Flags().Bool("dry-run", false, "Preview what would be created") templateInstantiateCmd.Flags().Bool("dry-run", false, "Preview what would be created")
templateInstantiateCmd.Flags().String("assignee", "", "Assign the root epic to this agent/user") templateInstantiateCmd.Flags().String("assignee", "", "Assign the root epic to this agent/user")
templateCmd.AddCommand(templateListCmd) templateCmd.AddCommand(templateListCmd)
templateCmd.AddCommand(templateShowCmd) templateCmd.AddCommand(templateShowCmd)
templateCmd.AddCommand(templateCreateCmd)
templateCmd.AddCommand(templateInstantiateCmd) templateCmd.AddCommand(templateInstantiateCmd)
rootCmd.AddCommand(templateCmd) rootCmd.AddCommand(templateCmd)
} }
// ============================================================================= // =============================================================================
// YAML Template Functions (for --from-template) // Beads Template Functions
// =============================================================================
// 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)
// ============================================================================= // =============================================================================
// loadTemplateSubgraph loads a template epic and all its descendants // loadTemplateSubgraph loads a template epic and all its descendants

View File

@@ -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")
}
}

View File

@@ -10,188 +10,6 @@ import (
"github.com/steveyegge/beads/internal/types" "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) // Beads Template Tests (for bd template instantiate)
// ============================================================================= // =============================================================================

View File

@@ -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

View File

@@ -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

View File

@@ -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