From 53790557de22535270126429f4548b9b58e490b7 Mon Sep 17 00:00:00 2001 From: quartz Date: Fri, 2 Jan 2026 14:00:31 -0800 Subject: [PATCH] test(cook): add gate bead creation tests (bd-4k3c) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- cmd/bd/cook_test.go | 314 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) diff --git a/cmd/bd/cook_test.go b/cmd/bd/cook_test.go index 27353da3..df9ee8d5 100644 --- a/cmd/bd/cook_test.go +++ b/cmd/bd/cook_test.go @@ -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) + } + } +}