Files
beads/cmd/bd/cook_test.go
quartz 53790557de test(cook): add gate bead creation tests (bd-4k3c)
Add comprehensive tests for gate bead creation during formula cooking:
- TestCreateGateIssue: tests gate issue creation with various types
  (gh:run, gh:pr, timer, human)
- TestCreateGateIssue_NilGate: verifies nil return for steps without Gate
- TestCreateGateIssue_Timeout: tests timeout parsing (30m, 1h, 24h, etc.)
- TestCookFormulaToSubgraph_GateBeads: tests gate beads in subgraph
- TestCookFormulaToSubgraph_GateDependencies: tests blocking deps
- TestCookFormulaToSubgraph_GateParentChild: tests parent-child deps

The gate bead implementation already existed in cook.go (createGateIssue
and collectSteps functions). These tests verify the behavior.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 14:01:56 -08:00

504 lines
13 KiB
Go

package main
import (
"testing"
"github.com/steveyegge/beads/internal/formula"
"github.com/steveyegge/beads/internal/types"
)
// =============================================================================
// Cook Tests (gt-8tmz.23: Compile-time vs Runtime Cooking)
// =============================================================================
// TestSubstituteFormulaVars tests variable substitution in formulas
func TestSubstituteFormulaVars(t *testing.T) {
tests := []struct {
name string
formula *formula.Formula
vars map[string]string
wantDesc string
wantStepTitle string
}{
{
name: "substitute single variable in description",
formula: &formula.Formula{
Description: "Build {{feature}} feature",
Steps: []*formula.Step{},
},
vars: map[string]string{"feature": "auth"},
wantDesc: "Build auth feature",
},
{
name: "substitute variable in step title",
formula: &formula.Formula{
Description: "Feature work",
Steps: []*formula.Step{
{Title: "Implement {{name}}"},
},
},
vars: map[string]string{"name": "login"},
wantDesc: "Feature work",
wantStepTitle: "Implement login",
},
{
name: "substitute multiple variables",
formula: &formula.Formula{
Description: "Release {{version}} on {{date}}",
Steps: []*formula.Step{
{Title: "Tag {{version}}"},
{Title: "Deploy to {{env}}"},
},
},
vars: map[string]string{
"version": "1.0.0",
"date": "2024-01-15",
"env": "production",
},
wantDesc: "Release 1.0.0 on 2024-01-15",
wantStepTitle: "Tag 1.0.0",
},
{
name: "nested children substitution",
formula: &formula.Formula{
Description: "Epic for {{project}}",
Steps: []*formula.Step{
{
Title: "Phase 1: {{project}} design",
Children: []*formula.Step{
{Title: "Design {{component}}"},
},
},
},
},
vars: map[string]string{
"project": "checkout",
"component": "cart",
},
wantDesc: "Epic for checkout",
wantStepTitle: "Phase 1: checkout design",
},
{
name: "unsubstituted variable left as-is",
formula: &formula.Formula{
Description: "Build {{feature}} with {{extra}}",
Steps: []*formula.Step{},
},
vars: map[string]string{"feature": "auth"},
wantDesc: "Build auth with {{extra}}", // {{extra}} unchanged
},
{
name: "empty vars map",
formula: &formula.Formula{
Description: "Keep {{placeholder}} intact",
Steps: []*formula.Step{},
},
vars: map[string]string{},
wantDesc: "Keep {{placeholder}} intact",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
substituteFormulaVars(tt.formula, tt.vars)
if tt.formula.Description != tt.wantDesc {
t.Errorf("Description = %q, want %q", tt.formula.Description, tt.wantDesc)
}
if tt.wantStepTitle != "" && len(tt.formula.Steps) > 0 {
if tt.formula.Steps[0].Title != tt.wantStepTitle {
t.Errorf("Steps[0].Title = %q, want %q", tt.formula.Steps[0].Title, tt.wantStepTitle)
}
}
})
}
}
// TestSubstituteStepVarsRecursive tests deep nesting works correctly
func TestSubstituteStepVarsRecursive(t *testing.T) {
steps := []*formula.Step{
{
Title: "Root: {{name}}",
Children: []*formula.Step{
{
Title: "Level 1: {{name}}",
Children: []*formula.Step{
{
Title: "Level 2: {{name}}",
Children: []*formula.Step{
{Title: "Level 3: {{name}}"},
},
},
},
},
},
},
}
vars := map[string]string{"name": "test"}
substituteStepVars(steps, vars)
// Check all levels got substituted
if steps[0].Title != "Root: test" {
t.Errorf("Root title = %q, want %q", steps[0].Title, "Root: test")
}
if steps[0].Children[0].Title != "Level 1: test" {
t.Errorf("Level 1 title = %q, want %q", steps[0].Children[0].Title, "Level 1: test")
}
if steps[0].Children[0].Children[0].Title != "Level 2: test" {
t.Errorf("Level 2 title = %q, want %q", steps[0].Children[0].Children[0].Title, "Level 2: test")
}
if steps[0].Children[0].Children[0].Children[0].Title != "Level 3: test" {
t.Errorf("Level 3 title = %q, want %q", steps[0].Children[0].Children[0].Children[0].Title, "Level 3: test")
}
}
// TestCompileTimeVsRuntimeMode tests that compile-time preserves placeholders
// and runtime mode substitutes them
func TestCompileTimeVsRuntimeMode(t *testing.T) {
// Simulate compile-time mode (no variable substitution)
compileFormula := &formula.Formula{
Description: "Feature: {{name}}",
Steps: []*formula.Step{
{Title: "Implement {{name}}"},
},
}
// In compile-time mode, don't call substituteFormulaVars
// Placeholders should remain intact
if compileFormula.Description != "Feature: {{name}}" {
t.Errorf("Compile-time: Description should preserve placeholder, got %q", compileFormula.Description)
}
// Simulate runtime mode (with variable substitution)
runtimeFormula := &formula.Formula{
Description: "Feature: {{name}}",
Steps: []*formula.Step{
{Title: "Implement {{name}}"},
},
}
vars := map[string]string{"name": "auth"}
substituteFormulaVars(runtimeFormula, vars)
if runtimeFormula.Description != "Feature: auth" {
t.Errorf("Runtime: Description = %q, want %q", runtimeFormula.Description, "Feature: auth")
}
if runtimeFormula.Steps[0].Title != "Implement auth" {
t.Errorf("Runtime: Steps[0].Title = %q, want %q", runtimeFormula.Steps[0].Title, "Implement auth")
}
}
// =============================================================================
// Gate Bead Tests (bd-4k3c: Gate beads created during cook)
// =============================================================================
// TestCreateGateIssue tests that createGateIssue creates proper gate issues
func TestCreateGateIssue(t *testing.T) {
tests := []struct {
name string
step *formula.Step
parentID string
wantID string
wantTitle string
wantAwaitType string
wantAwaitID string
}{
{
name: "gh:run gate with ID",
step: &formula.Step{
ID: "await-ci",
Title: "Wait for CI",
Gate: &formula.Gate{
Type: "gh:run",
ID: "release-build",
},
},
parentID: "mol-release",
wantID: "mol-release.gate-await-ci",
wantTitle: "Gate: gh:run release-build",
wantAwaitType: "gh:run",
wantAwaitID: "release-build",
},
{
name: "gh:pr gate without ID",
step: &formula.Step{
ID: "await-pr",
Title: "Wait for PR",
Gate: &formula.Gate{
Type: "gh:pr",
},
},
parentID: "mol-feature",
wantID: "mol-feature.gate-await-pr",
wantTitle: "Gate: gh:pr",
wantAwaitType: "gh:pr",
wantAwaitID: "",
},
{
name: "timer gate",
step: &formula.Step{
ID: "cooldown",
Title: "Wait for cooldown",
Gate: &formula.Gate{
Type: "timer",
Timeout: "30m",
},
},
parentID: "mol-deploy",
wantID: "mol-deploy.gate-cooldown",
wantTitle: "Gate: timer",
wantAwaitType: "timer",
wantAwaitID: "",
},
{
name: "human gate",
step: &formula.Step{
ID: "approval",
Title: "Manual approval",
Gate: &formula.Gate{
Type: "human",
Timeout: "24h",
},
},
parentID: "mol-release",
wantID: "mol-release.gate-approval",
wantTitle: "Gate: human",
wantAwaitType: "human",
wantAwaitID: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gateIssue := createGateIssue(tt.step, tt.parentID)
if gateIssue == nil {
t.Fatal("createGateIssue returned nil")
}
if gateIssue.ID != tt.wantID {
t.Errorf("ID = %q, want %q", gateIssue.ID, tt.wantID)
}
if gateIssue.Title != tt.wantTitle {
t.Errorf("Title = %q, want %q", gateIssue.Title, tt.wantTitle)
}
if gateIssue.AwaitType != tt.wantAwaitType {
t.Errorf("AwaitType = %q, want %q", gateIssue.AwaitType, tt.wantAwaitType)
}
if gateIssue.AwaitID != tt.wantAwaitID {
t.Errorf("AwaitID = %q, want %q", gateIssue.AwaitID, tt.wantAwaitID)
}
if gateIssue.IssueType != "gate" {
t.Errorf("IssueType = %q, want %q", gateIssue.IssueType, "gate")
}
if !gateIssue.IsTemplate {
t.Error("IsTemplate should be true")
}
})
}
}
// TestCreateGateIssue_NilGate tests that nil Gate returns nil
func TestCreateGateIssue_NilGate(t *testing.T) {
step := &formula.Step{
ID: "no-gate",
Title: "Step without gate",
Gate: nil,
}
gateIssue := createGateIssue(step, "mol-test")
if gateIssue != nil {
t.Errorf("Expected nil for step without Gate, got %+v", gateIssue)
}
}
// TestCreateGateIssue_Timeout tests that timeout is parsed correctly
func TestCreateGateIssue_Timeout(t *testing.T) {
tests := []struct {
name string
timeout string
wantMinutes int
}{
{"30 minutes", "30m", 30},
{"1 hour", "1h", 60},
{"24 hours", "24h", 1440},
{"invalid timeout", "invalid", 0},
{"empty timeout", "", 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
step := &formula.Step{
ID: "timed-step",
Title: "Timed step",
Gate: &formula.Gate{
Type: "timer",
Timeout: tt.timeout,
},
}
gateIssue := createGateIssue(step, "mol-test")
gotMinutes := int(gateIssue.Timeout.Minutes())
if gotMinutes != tt.wantMinutes {
t.Errorf("Timeout minutes = %d, want %d", gotMinutes, tt.wantMinutes)
}
})
}
}
// TestCookFormulaToSubgraph_GateBeads tests that gate beads are created in subgraph
func TestCookFormulaToSubgraph_GateBeads(t *testing.T) {
f := &formula.Formula{
Formula: "mol-test-gate",
Description: "Test gate creation",
Version: 1,
Type: formula.TypeWorkflow,
Steps: []*formula.Step{
{
ID: "build",
Title: "Build project",
},
{
ID: "await-ci",
Title: "Wait for CI",
Gate: &formula.Gate{
Type: "gh:run",
ID: "ci-workflow",
},
},
{
ID: "verify",
Title: "Verify deployment",
DependsOn: []string{"await-ci"},
},
},
}
subgraph, err := cookFormulaToSubgraph(f, "mol-test-gate")
if err != nil {
t.Fatalf("cookFormulaToSubgraph failed: %v", err)
}
// Should have: root + 3 steps + 1 gate = 5 issues
if len(subgraph.Issues) != 5 {
t.Errorf("Expected 5 issues, got %d", len(subgraph.Issues))
for _, issue := range subgraph.Issues {
t.Logf(" Issue: %s (%s)", issue.ID, issue.IssueType)
}
}
// Find the gate issue
var gateIssue *types.Issue
for _, issue := range subgraph.Issues {
if issue.IssueType == "gate" {
gateIssue = issue
break
}
}
if gateIssue == nil {
t.Fatal("Gate issue not found in subgraph")
}
if gateIssue.ID != "mol-test-gate.gate-await-ci" {
t.Errorf("Gate ID = %q, want %q", gateIssue.ID, "mol-test-gate.gate-await-ci")
}
if gateIssue.AwaitType != "gh:run" {
t.Errorf("Gate AwaitType = %q, want %q", gateIssue.AwaitType, "gh:run")
}
if gateIssue.AwaitID != "ci-workflow" {
t.Errorf("Gate AwaitID = %q, want %q", gateIssue.AwaitID, "ci-workflow")
}
}
// TestCookFormulaToSubgraph_GateDependencies tests that step depends on its gate
func TestCookFormulaToSubgraph_GateDependencies(t *testing.T) {
f := &formula.Formula{
Formula: "mol-gate-deps",
Description: "Test gate dependencies",
Version: 1,
Type: formula.TypeWorkflow,
Steps: []*formula.Step{
{
ID: "await-approval",
Title: "Wait for approval",
Gate: &formula.Gate{
Type: "human",
Timeout: "24h",
},
},
},
}
subgraph, err := cookFormulaToSubgraph(f, "mol-gate-deps")
if err != nil {
t.Fatalf("cookFormulaToSubgraph failed: %v", err)
}
// Find the blocking dependency: step -> gate
stepID := "mol-gate-deps.await-approval"
gateID := "mol-gate-deps.gate-await-approval"
var foundBlockingDep bool
for _, dep := range subgraph.Dependencies {
if dep.IssueID == stepID && dep.DependsOnID == gateID && dep.Type == "blocks" {
foundBlockingDep = true
break
}
}
if !foundBlockingDep {
t.Error("Expected blocking dependency from step to gate not found")
t.Log("Dependencies found:")
for _, dep := range subgraph.Dependencies {
t.Logf(" %s -> %s (%s)", dep.IssueID, dep.DependsOnID, dep.Type)
}
}
}
// TestCookFormulaToSubgraph_GateParentChild tests that gate is a child of the parent
func TestCookFormulaToSubgraph_GateParentChild(t *testing.T) {
f := &formula.Formula{
Formula: "mol-gate-parent",
Description: "Test gate parent-child relationship",
Version: 1,
Type: formula.TypeWorkflow,
Steps: []*formula.Step{
{
ID: "gated-step",
Title: "Gated step",
Gate: &formula.Gate{
Type: "mail",
},
},
},
}
subgraph, err := cookFormulaToSubgraph(f, "mol-gate-parent")
if err != nil {
t.Fatalf("cookFormulaToSubgraph failed: %v", err)
}
// Find the parent-child dependency: gate -> root
gateID := "mol-gate-parent.gate-gated-step"
rootID := "mol-gate-parent"
var foundParentChildDep bool
for _, dep := range subgraph.Dependencies {
if dep.IssueID == gateID && dep.DependsOnID == rootID && dep.Type == "parent-child" {
foundParentChildDep = true
break
}
}
if !foundParentChildDep {
t.Error("Expected parent-child dependency for gate not found")
t.Log("Dependencies found:")
for _, dep := range subgraph.Dependencies {
t.Logf(" %s -> %s (%s)", dep.IssueID, dep.DependsOnID, dep.Type)
}
}
}