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>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/beads/internal/formula"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
@@ -187,3 +188,316 @@ func TestCompileTimeVsRuntimeMode(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user