- Built-in templates: epic, bug, feature (embedded in binary) - Custom templates in .beads/templates/ (override built-ins) - Commands: bd template list/show/create - Flag: bd create --from-template <name> "Title" - Template fields: description, type, priority, labels, design, acceptance - Security: sanitize template names to prevent path traversal - Flag precedence: explicit flags override template defaults - Tests: template loading, security, flag precedence - Docs: commands/template.md and README.md updated Closes bd-164b Amp-Thread-ID: https://ampcode.com/threads/T-118fe54f-b112-4f99-a3d9-b7df53fb7284 Co-authored-by: Amp <amp@ampcode.com>
194 lines
4.6 KiB
Go
194 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
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()
|
|
oldWd, _ := os.Getwd()
|
|
defer os.Chdir(oldWd)
|
|
os.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()
|
|
oldWd, _ := os.Getwd()
|
|
defer os.Chdir(oldWd)
|
|
os.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)
|
|
}
|
|
})
|
|
}
|
|
}
|