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"
|
"testing"
|
||||||
|
|
||||||
"github.com/steveyegge/beads/internal/formula"
|
"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")
|
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