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:
quartz
2026-01-02 14:00:31 -08:00
committed by Steve Yegge
parent aaa8c3d032
commit 53790557de

View File

@@ -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)
}
}
}