- Add --assignee flag to `bd template instantiate` for work delegation - Assigns the root epic to specified agent/user - Child issues retain their template-defined assignees - Enables Gas Town orchestration: instantiate + assign in one step - Add missing --identity flag to `bd mail inbox` for consistency - All mail subcommands now support --identity override - Delete obsolete HANDOFF-template-redesign.md (work complete) - Add test for assignee override behavior 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
655 lines
18 KiB
Go
655 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
|
"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)
|
|
// =============================================================================
|
|
|
|
// TestExtractVariables tests the {{variable}} pattern extraction
|
|
func TestExtractVariables(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "single variable",
|
|
input: "Release {{version}}",
|
|
expected: []string{"version"},
|
|
},
|
|
{
|
|
name: "multiple variables",
|
|
input: "Release {{version}} on {{date}}",
|
|
expected: []string{"version", "date"},
|
|
},
|
|
{
|
|
name: "no variables",
|
|
input: "Just plain text",
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "duplicate variables",
|
|
input: "{{version}} and {{version}} again",
|
|
expected: []string{"version"},
|
|
},
|
|
{
|
|
name: "variable with underscore",
|
|
input: "{{my_variable}}",
|
|
expected: []string{"my_variable"},
|
|
},
|
|
{
|
|
name: "variable with numbers",
|
|
input: "{{var123}}",
|
|
expected: []string{"var123"},
|
|
},
|
|
{
|
|
name: "invalid variable format",
|
|
input: "{{123invalid}}",
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "empty braces",
|
|
input: "{{}}",
|
|
expected: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := extractVariables(tt.input)
|
|
if len(result) != len(tt.expected) {
|
|
t.Errorf("extractVariables(%q) = %v, want %v", tt.input, result, tt.expected)
|
|
return
|
|
}
|
|
for i, v := range result {
|
|
if v != tt.expected[i] {
|
|
t.Errorf("extractVariables(%q)[%d] = %q, want %q", tt.input, i, v, tt.expected[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSubstituteVariables tests the variable substitution
|
|
func TestSubstituteVariables(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
vars map[string]string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "single variable",
|
|
input: "Release {{version}}",
|
|
vars: map[string]string{"version": "1.2.0"},
|
|
expected: "Release 1.2.0",
|
|
},
|
|
{
|
|
name: "multiple variables",
|
|
input: "Release {{version}} on {{date}}",
|
|
vars: map[string]string{"version": "1.2.0", "date": "2024-01-15"},
|
|
expected: "Release 1.2.0 on 2024-01-15",
|
|
},
|
|
{
|
|
name: "missing variable unchanged",
|
|
input: "Release {{version}}",
|
|
vars: map[string]string{},
|
|
expected: "Release {{version}}",
|
|
},
|
|
{
|
|
name: "partial substitution",
|
|
input: "{{found}} and {{missing}}",
|
|
vars: map[string]string{"found": "yes"},
|
|
expected: "yes and {{missing}}",
|
|
},
|
|
{
|
|
name: "no variables",
|
|
input: "Just plain text",
|
|
vars: map[string]string{"version": "1.0"},
|
|
expected: "Just plain text",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := substituteVariables(tt.input, tt.vars)
|
|
if result != tt.expected {
|
|
t.Errorf("substituteVariables(%q, %v) = %q, want %q", tt.input, tt.vars, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// templateTestHelper provides helpers for Beads template tests
|
|
type templateTestHelper struct {
|
|
s *sqlite.SQLiteStorage
|
|
ctx context.Context
|
|
t *testing.T
|
|
}
|
|
|
|
func (h *templateTestHelper) createIssue(title, description string, issueType types.IssueType, priority int) *types.Issue {
|
|
issue := &types.Issue{
|
|
Title: title,
|
|
Description: description,
|
|
Priority: priority,
|
|
IssueType: issueType,
|
|
Status: types.StatusOpen,
|
|
}
|
|
if err := h.s.CreateIssue(h.ctx, issue, "test-user"); err != nil {
|
|
h.t.Fatalf("Failed to create issue: %v", err)
|
|
}
|
|
return issue
|
|
}
|
|
|
|
func (h *templateTestHelper) addParentChild(childID, parentID string) {
|
|
dep := &types.Dependency{
|
|
IssueID: childID,
|
|
DependsOnID: parentID,
|
|
Type: types.DepParentChild,
|
|
}
|
|
if err := h.s.AddDependency(h.ctx, dep, "test-user"); err != nil {
|
|
h.t.Fatalf("Failed to add parent-child dependency: %v", err)
|
|
}
|
|
}
|
|
|
|
func (h *templateTestHelper) addLabel(issueID, label string) {
|
|
if err := h.s.AddLabel(h.ctx, issueID, label, "test-user"); err != nil {
|
|
h.t.Fatalf("Failed to add label: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestLoadTemplateSubgraph tests loading a template epic with children
|
|
func TestLoadTemplateSubgraph(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "bd-test-template-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
testDB := filepath.Join(tmpDir, "test.db")
|
|
s := newTestStore(t, testDB)
|
|
defer s.Close()
|
|
|
|
ctx := context.Background()
|
|
h := &templateTestHelper{s: s, ctx: ctx, t: t}
|
|
|
|
t.Run("load epic with no children", func(t *testing.T) {
|
|
epic := h.createIssue("Template Epic", "Description", types.TypeEpic, 1)
|
|
h.addLabel(epic.ID, BeadsTemplateLabel)
|
|
|
|
subgraph, err := loadTemplateSubgraph(ctx, s, epic.ID)
|
|
if err != nil {
|
|
t.Fatalf("loadTemplateSubgraph failed: %v", err)
|
|
}
|
|
|
|
if subgraph.Root.ID != epic.ID {
|
|
t.Errorf("Root ID = %s, want %s", subgraph.Root.ID, epic.ID)
|
|
}
|
|
if len(subgraph.Issues) != 1 {
|
|
t.Errorf("Issues count = %d, want 1", len(subgraph.Issues))
|
|
}
|
|
})
|
|
|
|
t.Run("load epic with children", func(t *testing.T) {
|
|
epic := h.createIssue("Template {{name}}", "Epic for {{name}}", types.TypeEpic, 1)
|
|
h.addLabel(epic.ID, BeadsTemplateLabel)
|
|
|
|
child1 := h.createIssue("Task 1 for {{name}}", "", types.TypeTask, 2)
|
|
child2 := h.createIssue("Task 2 for {{name}}", "", types.TypeTask, 2)
|
|
h.addParentChild(child1.ID, epic.ID)
|
|
h.addParentChild(child2.ID, epic.ID)
|
|
|
|
subgraph, err := loadTemplateSubgraph(ctx, s, epic.ID)
|
|
if err != nil {
|
|
t.Fatalf("loadTemplateSubgraph failed: %v", err)
|
|
}
|
|
|
|
if len(subgraph.Issues) != 3 {
|
|
t.Errorf("Issues count = %d, want 3", len(subgraph.Issues))
|
|
}
|
|
|
|
// Check variables extracted
|
|
vars := extractAllVariables(subgraph)
|
|
if len(vars) != 1 || vars[0] != "name" {
|
|
t.Errorf("Variables = %v, want [name]", vars)
|
|
}
|
|
})
|
|
|
|
t.Run("load epic with nested children", func(t *testing.T) {
|
|
epic := h.createIssue("Nested Template", "", types.TypeEpic, 1)
|
|
child := h.createIssue("Child Task", "", types.TypeTask, 2)
|
|
grandchild := h.createIssue("Grandchild Task", "", types.TypeTask, 3)
|
|
|
|
h.addParentChild(child.ID, epic.ID)
|
|
h.addParentChild(grandchild.ID, child.ID)
|
|
|
|
subgraph, err := loadTemplateSubgraph(ctx, s, epic.ID)
|
|
if err != nil {
|
|
t.Fatalf("loadTemplateSubgraph failed: %v", err)
|
|
}
|
|
|
|
if len(subgraph.Issues) != 3 {
|
|
t.Errorf("Issues count = %d, want 3 (epic + child + grandchild)", len(subgraph.Issues))
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestCloneSubgraph tests cloning a template with variable substitution
|
|
func TestCloneSubgraph(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "bd-test-clone-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
testDB := filepath.Join(tmpDir, "test.db")
|
|
s := newTestStore(t, testDB)
|
|
defer s.Close()
|
|
|
|
ctx := context.Background()
|
|
h := &templateTestHelper{s: s, ctx: ctx, t: t}
|
|
|
|
t.Run("clone simple template", func(t *testing.T) {
|
|
epic := h.createIssue("Release {{version}}", "Release notes for {{version}}", types.TypeEpic, 1)
|
|
h.addLabel(epic.ID, BeadsTemplateLabel)
|
|
|
|
subgraph, err := loadTemplateSubgraph(ctx, s, epic.ID)
|
|
if err != nil {
|
|
t.Fatalf("loadTemplateSubgraph failed: %v", err)
|
|
}
|
|
|
|
vars := map[string]string{"version": "2.0.0"}
|
|
result, err := cloneSubgraph(ctx, s, subgraph, vars, "", "test-user")
|
|
if err != nil {
|
|
t.Fatalf("cloneSubgraph failed: %v", err)
|
|
}
|
|
|
|
if result.Created != 1 {
|
|
t.Errorf("Created = %d, want 1", result.Created)
|
|
}
|
|
if result.NewEpicID == epic.ID {
|
|
t.Error("NewEpicID should be different from template ID")
|
|
}
|
|
|
|
// Verify the cloned issue
|
|
newEpic, err := s.GetIssue(ctx, result.NewEpicID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get cloned issue: %v", err)
|
|
}
|
|
if newEpic.Title != "Release 2.0.0" {
|
|
t.Errorf("Title = %q, want %q", newEpic.Title, "Release 2.0.0")
|
|
}
|
|
if newEpic.Description != "Release notes for 2.0.0" {
|
|
t.Errorf("Description = %q, want %q", newEpic.Description, "Release notes for 2.0.0")
|
|
}
|
|
})
|
|
|
|
t.Run("clone template with children", func(t *testing.T) {
|
|
epic := h.createIssue("Deploy {{service}}", "", types.TypeEpic, 1)
|
|
child1 := h.createIssue("Build {{service}}", "", types.TypeTask, 2)
|
|
child2 := h.createIssue("Test {{service}}", "", types.TypeTask, 2)
|
|
|
|
h.addParentChild(child1.ID, epic.ID)
|
|
h.addParentChild(child2.ID, epic.ID)
|
|
h.addLabel(epic.ID, BeadsTemplateLabel)
|
|
|
|
subgraph, err := loadTemplateSubgraph(ctx, s, epic.ID)
|
|
if err != nil {
|
|
t.Fatalf("loadTemplateSubgraph failed: %v", err)
|
|
}
|
|
|
|
vars := map[string]string{"service": "api-gateway"}
|
|
result, err := cloneSubgraph(ctx, s, subgraph, vars, "", "test-user")
|
|
if err != nil {
|
|
t.Fatalf("cloneSubgraph failed: %v", err)
|
|
}
|
|
|
|
if result.Created != 3 {
|
|
t.Errorf("Created = %d, want 3", result.Created)
|
|
}
|
|
|
|
// Verify all IDs are different
|
|
if _, ok := result.IDMapping[epic.ID]; !ok {
|
|
t.Error("ID mapping missing epic")
|
|
}
|
|
if _, ok := result.IDMapping[child1.ID]; !ok {
|
|
t.Error("ID mapping missing child1")
|
|
}
|
|
if _, ok := result.IDMapping[child2.ID]; !ok {
|
|
t.Error("ID mapping missing child2")
|
|
}
|
|
|
|
// Verify cloned epic title
|
|
newEpic, err := s.GetIssue(ctx, result.NewEpicID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get cloned epic: %v", err)
|
|
}
|
|
if newEpic.Title != "Deploy api-gateway" {
|
|
t.Errorf("Epic title = %q, want %q", newEpic.Title, "Deploy api-gateway")
|
|
}
|
|
|
|
// Verify dependencies were cloned
|
|
deps, err := s.GetDependencyRecords(ctx, result.IDMapping[child1.ID])
|
|
if err != nil {
|
|
t.Fatalf("Failed to get dependencies: %v", err)
|
|
}
|
|
hasParentChild := false
|
|
for _, dep := range deps {
|
|
if dep.DependsOnID == result.NewEpicID && dep.Type == types.DepParentChild {
|
|
hasParentChild = true
|
|
break
|
|
}
|
|
}
|
|
if !hasParentChild {
|
|
t.Error("Cloned child should have parent-child dependency on cloned epic")
|
|
}
|
|
})
|
|
|
|
t.Run("cloned issues start with open status", func(t *testing.T) {
|
|
// Create template with in_progress status
|
|
epic := h.createIssue("Template", "", types.TypeEpic, 1)
|
|
err := s.UpdateIssue(ctx, epic.ID, map[string]interface{}{"status": "in_progress"}, "test-user")
|
|
if err != nil {
|
|
t.Fatalf("Failed to update status: %v", err)
|
|
}
|
|
|
|
subgraph, err := loadTemplateSubgraph(ctx, s, epic.ID)
|
|
if err != nil {
|
|
t.Fatalf("loadTemplateSubgraph failed: %v", err)
|
|
}
|
|
|
|
result, err := cloneSubgraph(ctx, s, subgraph, nil, "", "test-user")
|
|
if err != nil {
|
|
t.Fatalf("cloneSubgraph failed: %v", err)
|
|
}
|
|
|
|
newEpic, err := s.GetIssue(ctx, result.NewEpicID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get cloned issue: %v", err)
|
|
}
|
|
if newEpic.Status != types.StatusOpen {
|
|
t.Errorf("Status = %s, want %s", newEpic.Status, types.StatusOpen)
|
|
}
|
|
})
|
|
|
|
t.Run("assignee override applies to root epic only", func(t *testing.T) {
|
|
epic := h.createIssue("Root Epic", "", types.TypeEpic, 1)
|
|
child := h.createIssue("Child Task", "", types.TypeTask, 2)
|
|
h.addParentChild(child.ID, epic.ID)
|
|
|
|
// Set assignees on template
|
|
err := s.UpdateIssue(ctx, epic.ID, map[string]interface{}{"assignee": "template-owner"}, "test-user")
|
|
if err != nil {
|
|
t.Fatalf("Failed to set epic assignee: %v", err)
|
|
}
|
|
err = s.UpdateIssue(ctx, child.ID, map[string]interface{}{"assignee": "child-owner"}, "test-user")
|
|
if err != nil {
|
|
t.Fatalf("Failed to set child assignee: %v", err)
|
|
}
|
|
|
|
subgraph, err := loadTemplateSubgraph(ctx, s, epic.ID)
|
|
if err != nil {
|
|
t.Fatalf("loadTemplateSubgraph failed: %v", err)
|
|
}
|
|
|
|
// Clone with assignee override
|
|
result, err := cloneSubgraph(ctx, s, subgraph, nil, "new-assignee", "test-user")
|
|
if err != nil {
|
|
t.Fatalf("cloneSubgraph failed: %v", err)
|
|
}
|
|
|
|
// Root epic should have override assignee
|
|
newEpic, err := s.GetIssue(ctx, result.NewEpicID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get cloned epic: %v", err)
|
|
}
|
|
if newEpic.Assignee != "new-assignee" {
|
|
t.Errorf("Epic assignee = %q, want %q", newEpic.Assignee, "new-assignee")
|
|
}
|
|
|
|
// Child should keep template assignee
|
|
newChildID := result.IDMapping[child.ID]
|
|
newChild, err := s.GetIssue(ctx, newChildID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get cloned child: %v", err)
|
|
}
|
|
if newChild.Assignee != "child-owner" {
|
|
t.Errorf("Child assignee = %q, want %q", newChild.Assignee, "child-owner")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestExtractAllVariables tests extracting variables from entire subgraph
|
|
func TestExtractAllVariables(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "bd-test-extractall-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
testDB := filepath.Join(tmpDir, "test.db")
|
|
s := newTestStore(t, testDB)
|
|
defer s.Close()
|
|
|
|
ctx := context.Background()
|
|
h := &templateTestHelper{s: s, ctx: ctx, t: t}
|
|
|
|
epic := h.createIssue("Release {{version}}", "For {{product}}", types.TypeEpic, 1)
|
|
child := h.createIssue("Deploy to {{environment}}", "", types.TypeTask, 2)
|
|
h.addParentChild(child.ID, epic.ID)
|
|
|
|
subgraph, err := loadTemplateSubgraph(ctx, s, epic.ID)
|
|
if err != nil {
|
|
t.Fatalf("loadTemplateSubgraph failed: %v", err)
|
|
}
|
|
|
|
vars := extractAllVariables(subgraph)
|
|
|
|
// Should find version, product, and environment
|
|
varMap := make(map[string]bool)
|
|
for _, v := range vars {
|
|
varMap[v] = true
|
|
}
|
|
|
|
if !varMap["version"] {
|
|
t.Error("Missing variable: version")
|
|
}
|
|
if !varMap["product"] {
|
|
t.Error("Missing variable: product")
|
|
}
|
|
if !varMap["environment"] {
|
|
t.Error("Missing variable: environment")
|
|
}
|
|
}
|