fix(molecule): use Dependencies from bd show instead of empty DependsOn (#901)

* fix(molecule): use Dependencies from bd show instead of empty DependsOn

Bug: Molecule step dependency checking was broken because bd list
doesn't populate the DependsOn field (it's always empty). Only bd show
returns dependency info in the Dependencies field.

This caused all open steps to appear "ready" regardless of actual
dependencies - the polecat would start blocked steps prematurely.

Fix: Call ShowMultiple() after List() to fetch full issue details
including Dependencies, then check Dependencies instead of DependsOn.

Affected functions:
- findNextReadyStep() in molecule_step.go
- getMoleculeProgressInfo() in molecule_status.go
- runMoleculeCurrent() in molecule_status.go

Tests:
- Added TestFindNextReadyStepWithBdListBehavior to verify fix
- Added TestOldBuggyBehavior to demonstrate the bug
- Updated existing tests to use fixed algorithm

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(molecule): use Dependencies from bd show instead of empty DependsOn

Bug: Molecule step dependency checking was broken because bd list
doesn't populate the DependsOn field (it's always empty). Only bd show
returns dependency info in the Dependencies field.

This caused all open steps to appear "ready" regardless of actual
dependencies - the polecat would start blocked steps prematurely.

Fix: Call ShowMultiple() after List() to fetch full issue details
including Dependencies, then check Dependencies instead of DependsOn.
Also filter to only check "blocks" type dependencies - ignore "parent-child"
relationships which are just structural, not blocking.

Affected functions:
- findNextReadyStep() in molecule_step.go
- getMoleculeProgressInfo() in molecule_status.go
- runMoleculeCurrent() in molecule_status.go

Tests:
- Added TestFindNextReadyStepWithBdListBehavior to verify fix
- Added TestOldBuggyBehavior to demonstrate the bug
- Updated existing tests to use fixed algorithm

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: julianknutsen <julianknutsen@users.noreply.github>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Julian Knutsen
2026-01-24 19:46:27 -10:00
committed by GitHub
parent 4e4824a6c6
commit 3da0d5a7c8
3 changed files with 444 additions and 55 deletions

View File

@@ -184,11 +184,25 @@ func runMoleculeProgress(cmd *cobra.Command, args []string) error {
}
}
// Build set of closed issue IDs for dependency checking
// Build set of closed issue IDs and collect open step IDs for dependency checking
closedIDs := make(map[string]bool)
var openStepIDs []string
for _, child := range children {
if child.Status == "closed" {
closedIDs[child.ID] = true
} else if child.Status == "open" {
openStepIDs = append(openStepIDs, child.ID)
}
}
// Fetch full details for open steps to get dependency info.
// bd list doesn't return dependencies, but bd show does.
var openStepsMap map[string]*beads.Issue
if len(openStepIDs) > 0 {
openStepsMap, err = b.ShowMultiple(openStepIDs)
if err != nil {
// Non-fatal: continue without dependency info (all open steps will be "ready")
openStepsMap = make(map[string]*beads.Issue)
}
}
@@ -202,16 +216,30 @@ func runMoleculeProgress(cmd *cobra.Command, args []string) error {
case "in_progress":
progress.InProgress++
case "open":
// Check if all dependencies are closed
// Get full step info with dependencies
step := openStepsMap[child.ID]
// Check if all dependencies are closed using Dependencies field
// (from bd show), not DependsOn (which is empty from bd list).
// Only "blocks" type dependencies block progress - ignore "parent-child".
allDepsClosed := true
for _, depID := range child.DependsOn {
if !closedIDs[depID] {
hasBlockingDeps := false
var deps []beads.IssueDep
if step != nil {
deps = step.Dependencies
}
for _, dep := range deps {
if dep.DependencyType != "blocks" {
continue // Skip parent-child and other non-blocking relationships
}
hasBlockingDeps = true
if !closedIDs[dep.ID] {
allDepsClosed = false
break
}
}
if len(child.DependsOn) == 0 || allDepsClosed {
if !hasBlockingDeps || allDepsClosed {
progress.ReadySteps = append(progress.ReadySteps, child.ID)
} else {
progress.BlockedSteps = append(progress.BlockedSteps, child.ID)
@@ -509,11 +537,25 @@ func getMoleculeProgressInfo(b *beads.Beads, moleculeRootID string) (*MoleculePr
}
}
// Build set of closed issue IDs for dependency checking
// Build set of closed issue IDs and collect open step IDs for dependency checking
closedIDs := make(map[string]bool)
var openStepIDs []string
for _, child := range children {
if child.Status == "closed" {
closedIDs[child.ID] = true
} else if child.Status == "open" {
openStepIDs = append(openStepIDs, child.ID)
}
}
// Fetch full details for open steps to get dependency info.
// bd list doesn't return dependencies, but bd show does.
var openStepsMap map[string]*beads.Issue
if len(openStepIDs) > 0 {
openStepsMap, err = b.ShowMultiple(openStepIDs)
if err != nil {
// Non-fatal: continue without dependency info (all open steps will be "ready")
openStepsMap = make(map[string]*beads.Issue)
}
}
@@ -527,16 +569,30 @@ func getMoleculeProgressInfo(b *beads.Beads, moleculeRootID string) (*MoleculePr
case "in_progress":
progress.InProgress++
case "open":
// Check if all dependencies are closed
// Get full step info with dependencies
step := openStepsMap[child.ID]
// Check if all dependencies are closed using Dependencies field
// (from bd show), not DependsOn (which is empty from bd list).
// Only "blocks" type dependencies block progress - ignore "parent-child".
allDepsClosed := true
for _, depID := range child.DependsOn {
if !closedIDs[depID] {
hasBlockingDeps := false
var deps []beads.IssueDep
if step != nil {
deps = step.Dependencies
}
for _, dep := range deps {
if dep.DependencyType != "blocks" {
continue // Skip parent-child and other non-blocking relationships
}
hasBlockingDeps = true
if !closedIDs[dep.ID] {
allDepsClosed = false
break
}
}
if len(child.DependsOn) == 0 || allDepsClosed {
if !hasBlockingDeps || allDepsClosed {
progress.ReadySteps = append(progress.ReadySteps, child.ID)
} else {
progress.BlockedSteps = append(progress.BlockedSteps, child.ID)
@@ -774,10 +830,10 @@ func runMoleculeCurrent(cmd *cobra.Command, args []string) error {
info.StepsTotal = len(children)
// Build set of closed issue IDs for dependency checking
// Build set of closed issue IDs and collect open step IDs for dependency checking
closedIDs := make(map[string]bool)
var inProgressSteps []*beads.Issue
var readySteps []*beads.Issue
var openStepIDs []string
for _, child := range children {
switch child.Status {
@@ -786,23 +842,47 @@ func runMoleculeCurrent(cmd *cobra.Command, args []string) error {
closedIDs[child.ID] = true
case "in_progress":
inProgressSteps = append(inProgressSteps, child)
case "open":
openStepIDs = append(openStepIDs, child.ID)
}
}
// Fetch full details for open steps to get dependency info.
// bd list doesn't return dependencies, but bd show does.
var openStepsMap map[string]*beads.Issue
if len(openStepIDs) > 0 {
openStepsMap, _ = b.ShowMultiple(openStepIDs)
if openStepsMap == nil {
openStepsMap = make(map[string]*beads.Issue)
}
}
// Find ready steps (open with all deps closed)
for _, child := range children {
if child.Status == "open" {
allDepsClosed := true
for _, depID := range child.DependsOn {
if !closedIDs[depID] {
allDepsClosed = false
break
}
var readySteps []*beads.Issue
for _, stepID := range openStepIDs {
step := openStepsMap[stepID]
if step == nil {
continue
}
// Check dependencies using Dependencies field (from bd show),
// not DependsOn (which is empty from bd list).
// Only "blocks" type dependencies block progress - ignore "parent-child".
allDepsClosed := true
hasBlockingDeps := false
for _, dep := range step.Dependencies {
if dep.DependencyType != "blocks" {
continue // Skip parent-child and other non-blocking relationships
}
if len(child.DependsOn) == 0 || allDepsClosed {
readySteps = append(readySteps, child)
hasBlockingDeps = true
if !closedIDs[dep.ID] {
allDepsClosed = false
break
}
}
if !hasBlockingDeps || allDepsClosed {
readySteps = append(readySteps, step)
}
}
// Determine current step and status

View File

@@ -205,11 +205,11 @@ func findNextReadyStep(b *beads.Beads, moleculeID string) (*beads.Issue, bool, e
return nil, true, nil // No steps = complete
}
// Build set of closed step IDs and collect open steps
// Build set of closed step IDs and collect open step IDs
// Note: "open" means not started. "in_progress" means someone's working on it.
// We only consider "open" steps as candidates for the next step.
closedIDs := make(map[string]bool)
var openSteps []*beads.Issue
var openStepIDs []string
hasNonClosedSteps := false
for _, child := range children {
@@ -217,7 +217,7 @@ func findNextReadyStep(b *beads.Beads, moleculeID string) (*beads.Issue, bool, e
case "closed":
closedIDs[child.ID] = true
case "open":
openSteps = append(openSteps, child)
openStepIDs = append(openStepIDs, child.ID)
hasNonClosedSteps = true
default:
// in_progress or other status - not closed, not available
@@ -230,17 +230,42 @@ func findNextReadyStep(b *beads.Beads, moleculeID string) (*beads.Issue, bool, e
return nil, true, nil
}
// No open steps to check
if len(openStepIDs) == 0 {
return nil, false, nil
}
// Fetch full details for open steps to get dependency info.
// bd list doesn't return dependencies, but bd show does.
openStepsMap, err := b.ShowMultiple(openStepIDs)
if err != nil {
return nil, false, fmt.Errorf("fetching step details: %w", err)
}
// Find ready steps (open steps with all dependencies closed)
for _, step := range openSteps {
for _, stepID := range openStepIDs {
step, ok := openStepsMap[stepID]
if !ok {
continue
}
// Check dependencies using the Dependencies field (from bd show),
// not DependsOn (which is empty from bd list).
// Only "blocks" type dependencies block progress - ignore "parent-child".
allDepsClosed := true
for _, depID := range step.DependsOn {
if !closedIDs[depID] {
hasBlockingDeps := false
for _, dep := range step.Dependencies {
if dep.DependencyType != "blocks" {
continue // Skip parent-child and other non-blocking relationships
}
hasBlockingDeps = true
if !closedIDs[dep.ID] {
allDepsClosed = false
break
}
}
if len(step.DependsOn) == 0 || allDepsClosed {
if !hasBlockingDeps || allDepsClosed {
return step, false, nil
}
}

View File

@@ -79,7 +79,10 @@ func TestExtractMoleculeIDFromStep(t *testing.T) {
}
}
// mockBeadsForStep extends mockBeads with parent filtering for step tests
// mockBeadsForStep extends mockBeads with parent filtering for step tests.
// It simulates the real bd behavior where:
// - List() returns issues with DependsOn empty (bd list doesn't return deps)
// - Show()/ShowMultiple() returns issues with Dependencies populated (bd show does)
type mockBeadsForStep struct {
issues map[string]*beads.Issue
}
@@ -101,6 +104,19 @@ func (m *mockBeadsForStep) Show(id string) (*beads.Issue, error) {
return nil, beads.ErrNotFound
}
// ShowMultiple simulates bd show with multiple IDs - returns full issue data including Dependencies
func (m *mockBeadsForStep) ShowMultiple(ids []string) (map[string]*beads.Issue, error) {
result := make(map[string]*beads.Issue)
for _, id := range ids {
if issue, ok := m.issues[id]; ok {
result[id] = issue
}
}
return result, nil
}
// List simulates bd list behavior - returns issues but with DependsOn EMPTY.
// This is the key behavior that caused the bug: bd list doesn't return dependency info.
func (m *mockBeadsForStep) List(opts beads.ListOptions) ([]*beads.Issue, error) {
var result []*beads.Issue
for _, issue := range m.issues {
@@ -112,7 +128,11 @@ func (m *mockBeadsForStep) List(opts beads.ListOptions) ([]*beads.Issue, error)
if opts.Status != "" && opts.Status != "all" && issue.Status != opts.Status {
continue
}
result = append(result, issue)
// CRITICAL: Simulate bd list behavior - DependsOn is NOT populated
// Create a copy with empty DependsOn to simulate real bd list output
issueCopy := *issue
issueCopy.DependsOn = nil // bd list doesn't return this
result = append(result, &issueCopy)
}
return result, nil
}
@@ -128,19 +148,34 @@ func (m *mockBeadsForStep) Close(ids ...string) error {
return nil
}
// makeStepIssue creates a test step issue
// makeStepIssue creates a test step issue with both DependsOn and Dependencies set.
// In real usage:
// - bd list returns issues with DependsOn empty
// - bd show returns issues with Dependencies populated (with DependencyType)
// The mock simulates this: List() clears DependsOn, Show() returns the full issue.
func makeStepIssue(id, title, parent, status string, dependsOn []string) *beads.Issue {
return &beads.Issue{
issue := &beads.Issue{
ID: id,
Title: title,
Type: "task",
Status: status,
Priority: 2,
Parent: parent,
DependsOn: dependsOn,
DependsOn: dependsOn, // This gets cleared by mock List() to simulate bd list
CreatedAt: "2025-01-01T12:00:00Z",
UpdatedAt: "2025-01-01T12:00:00Z",
}
// Also set Dependencies (what bd show returns) for proper testing.
// Use "blocks" dependency type since that's what formula instantiation creates
// for inter-step dependencies (vs "parent-child" for parent relationships).
for _, depID := range dependsOn {
issue.Dependencies = append(issue.Dependencies, beads.IssueDep{
ID: depID,
Title: "Dependency " + depID,
DependencyType: "blocks", // Only "blocks" deps should block progress
})
}
return issue
}
func TestFindNextReadyStep(t *testing.T) {
@@ -232,24 +267,22 @@ func TestFindNextReadyStep(t *testing.T) {
m := newMockBeadsForStep()
tt.setupFunc(m)
// Create a real Beads instance but we'll use our mock
// For now, we test the logic by calling the actual function with mock data
// This requires refactoring findNextReadyStep to accept an interface
// For now, we'll test the logic inline
// Test the FIXED algorithm that uses ShowMultiple for dependency info
// (simulating the real findNextReadyStep behavior after the fix)
// Get children from mock
// Get children from mock (DependsOn will be empty - simulating bd list)
children, _ := m.List(beads.ListOptions{Parent: tt.moleculeID, Status: "all"})
// Build closed IDs set - only "open" steps are candidates
// Build closed IDs set and collect open step IDs
closedIDs := make(map[string]bool)
var openSteps []*beads.Issue
var openStepIDs []string
hasNonClosedSteps := false
for _, child := range children {
switch child.Status {
case "closed":
closedIDs[child.ID] = true
case "open":
openSteps = append(openSteps, child)
openStepIDs = append(openStepIDs, child.ID)
hasNonClosedSteps = true
default:
// in_progress or other - not closed, not available
@@ -268,17 +301,32 @@ func TestFindNextReadyStep(t *testing.T) {
return
}
// Find ready step
// Fetch full details for open steps (Dependencies will be populated)
openStepsMap, _ := m.ShowMultiple(openStepIDs)
// Find ready step using Dependencies (not DependsOn!)
// Only "blocks" type dependencies block progress - ignore "parent-child".
var readyStep *beads.Issue
for _, step := range openSteps {
for _, stepID := range openStepIDs {
step := openStepsMap[stepID]
if step == nil {
continue
}
// Use Dependencies (from bd show), NOT DependsOn (empty from bd list)
allDepsClosed := true
for _, depID := range step.DependsOn {
if !closedIDs[depID] {
hasBlockingDeps := false
for _, dep := range step.Dependencies {
if dep.DependencyType != "blocks" {
continue // Skip parent-child and other non-blocking relationships
}
hasBlockingDeps = true
if !closedIDs[dep.ID] {
allDepsClosed = false
break
}
}
if len(step.DependsOn) == 0 || allDepsClosed {
if !hasBlockingDeps || allDepsClosed {
readyStep = step
break
}
@@ -372,18 +420,18 @@ func TestStepDoneScenarios(t *testing.T) {
t.Fatalf("failed to close step: %v", err)
}
// Now find next ready step
// Now find next ready step using the FIXED algorithm
children, _ := m.List(beads.ListOptions{Parent: moleculeID, Status: "all"})
closedIDs := make(map[string]bool)
var openSteps []*beads.Issue
var openStepIDs []string
hasNonClosedSteps := false
for _, child := range children {
switch child.Status {
case "closed":
closedIDs[child.ID] = true
case "open":
openSteps = append(openSteps, child)
openStepIDs = append(openStepIDs, child.ID)
hasNonClosedSteps = true
default:
// in_progress or other - not closed, not available
@@ -399,17 +447,32 @@ func TestStepDoneScenarios(t *testing.T) {
if allComplete {
action = "done"
} else {
// Find ready step
// Fetch full details for open steps (Dependencies will be populated)
openStepsMap, _ := m.ShowMultiple(openStepIDs)
// Find ready step using Dependencies (not DependsOn!)
// Only "blocks" type dependencies block progress - ignore "parent-child".
var readyStep *beads.Issue
for _, step := range openSteps {
for _, stepID := range openStepIDs {
step := openStepsMap[stepID]
if step == nil {
continue
}
// Use Dependencies (from bd show), NOT DependsOn (empty from bd list)
allDepsClosed := true
for _, depID := range step.DependsOn {
if !closedIDs[depID] {
hasBlockingDeps := false
for _, dep := range step.Dependencies {
if dep.DependencyType != "blocks" {
continue // Skip parent-child and other non-blocking relationships
}
hasBlockingDeps = true
if !closedIDs[dep.ID] {
allDepsClosed = false
break
}
}
if len(step.DependsOn) == 0 || allDepsClosed {
if !hasBlockingDeps || allDepsClosed {
readyStep = step
break
}
@@ -433,3 +496,224 @@ func TestStepDoneScenarios(t *testing.T) {
})
}
}
// TestFindNextReadyStepWithBdListBehavior tests the fix for the bug where
// bd list doesn't return dependency info (DependsOn is always empty), but
// bd show returns Dependencies. The old code checked DependsOn (always empty),
// so all open steps looked "ready" even when blocked.
//
// This test simulates real bd behavior and verifies the fix works correctly.
func TestFindNextReadyStepWithBdListBehavior(t *testing.T) {
tests := []struct {
name string
moleculeID string
setupFunc func(*mockBeadsForStep)
wantStepID string // Expected ready step ID, or "" if none ready
wantComplete bool
wantBlocked bool // True if all remaining steps are blocked (none ready)
}{
{
name: "blocked step should NOT be ready - dependency not closed",
moleculeID: "gt-mol",
setupFunc: func(m *mockBeadsForStep) {
// Step 1 is open (first step, no deps)
m.addIssue(makeStepIssue("gt-mol.1", "Step 1", "gt-mol", "open", nil))
// Step 2 depends on Step 1, which is NOT closed
// BUG: Old code would mark Step 2 as ready because DependsOn is empty from bd list
// FIX: New code uses Dependencies from bd show
m.addIssue(makeStepIssue("gt-mol.2", "Step 2", "gt-mol", "open", []string{"gt-mol.1"}))
},
wantStepID: "gt-mol.1", // Only step 1 should be ready
wantComplete: false,
},
{
name: "step becomes ready when dependency closes",
moleculeID: "gt-mol",
setupFunc: func(m *mockBeadsForStep) {
m.addIssue(makeStepIssue("gt-mol.1", "Step 1", "gt-mol", "closed", nil))
m.addIssue(makeStepIssue("gt-mol.2", "Step 2", "gt-mol", "open", []string{"gt-mol.1"}))
},
wantStepID: "gt-mol.2", // Step 2 is ready now that step 1 is closed
wantComplete: false,
},
{
name: "multiple blocked steps - none ready",
moleculeID: "gt-mol",
setupFunc: func(m *mockBeadsForStep) {
// Step 1 is in_progress (not closed)
m.addIssue(makeStepIssue("gt-mol.1", "Step 1", "gt-mol", "in_progress", nil))
// Steps 2 and 3 both depend on step 1
m.addIssue(makeStepIssue("gt-mol.2", "Step 2", "gt-mol", "open", []string{"gt-mol.1"}))
m.addIssue(makeStepIssue("gt-mol.3", "Step 3", "gt-mol", "open", []string{"gt-mol.1"}))
},
wantBlocked: true, // No open steps are ready (all blocked by step 1)
wantComplete: false,
},
{
name: "diamond dependency - synthesis blocked until both complete",
moleculeID: "gt-mol",
setupFunc: func(m *mockBeadsForStep) {
m.addIssue(makeStepIssue("gt-mol.1", "Step A", "gt-mol", "closed", nil))
m.addIssue(makeStepIssue("gt-mol.2", "Step B", "gt-mol", "open", nil))
// Synthesis depends on BOTH A and B
m.addIssue(makeStepIssue("gt-mol.3", "Synthesis", "gt-mol", "open", []string{"gt-mol.1", "gt-mol.2"}))
},
wantStepID: "gt-mol.2", // B is ready (no deps), synthesis is blocked
wantComplete: false,
},
{
name: "diamond dependency - synthesis ready when both complete",
moleculeID: "gt-mol",
setupFunc: func(m *mockBeadsForStep) {
m.addIssue(makeStepIssue("gt-mol.1", "Step A", "gt-mol", "closed", nil))
m.addIssue(makeStepIssue("gt-mol.2", "Step B", "gt-mol", "closed", nil))
// Synthesis depends on BOTH A and B, both are now closed
m.addIssue(makeStepIssue("gt-mol.3", "Synthesis", "gt-mol", "open", []string{"gt-mol.1", "gt-mol.2"}))
},
wantStepID: "gt-mol.3", // Synthesis is now ready
wantComplete: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := newMockBeadsForStep()
tt.setupFunc(m)
// Simulate the FIXED algorithm that uses ShowMultiple for dependency info
// Step 1: List children (DependsOn will be empty - simulating bd list)
children, _ := m.List(beads.ListOptions{Parent: tt.moleculeID, Status: "all"})
// Build closed IDs and collect open step IDs
closedIDs := make(map[string]bool)
var openStepIDs []string
hasNonClosedSteps := false
for _, child := range children {
switch child.Status {
case "closed":
closedIDs[child.ID] = true
case "open":
openStepIDs = append(openStepIDs, child.ID)
hasNonClosedSteps = true
default:
hasNonClosedSteps = true
}
}
allComplete := !hasNonClosedSteps
if allComplete != tt.wantComplete {
t.Errorf("allComplete = %v, want %v", allComplete, tt.wantComplete)
}
if tt.wantComplete {
return
}
// Step 2: Fetch full details for open steps (Dependencies will be populated)
openStepsMap, _ := m.ShowMultiple(openStepIDs)
// Step 3: Find ready step using Dependencies (not DependsOn!)
// Only "blocks" type dependencies block progress - ignore "parent-child".
var readyStep *beads.Issue
for _, stepID := range openStepIDs {
step := openStepsMap[stepID]
if step == nil {
continue
}
// Use Dependencies (from bd show), NOT DependsOn (empty from bd list)
allDepsClosed := true
hasBlockingDeps := false
for _, dep := range step.Dependencies {
if dep.DependencyType != "blocks" {
continue // Skip parent-child and other non-blocking relationships
}
hasBlockingDeps = true
if !closedIDs[dep.ID] {
allDepsClosed = false
break
}
}
if !hasBlockingDeps || allDepsClosed {
readyStep = step
break
}
}
// Verify results
if tt.wantBlocked {
if readyStep != nil {
t.Errorf("expected no ready steps (all blocked), got %s", readyStep.ID)
}
return
}
if tt.wantStepID == "" {
if readyStep != nil {
t.Errorf("expected no ready step, got %s", readyStep.ID)
}
return
}
if readyStep == nil {
t.Errorf("expected ready step %s, got nil", tt.wantStepID)
return
}
if readyStep.ID != tt.wantStepID {
t.Errorf("ready step = %s, want %s", readyStep.ID, tt.wantStepID)
}
})
}
}
// TestOldBuggyBehavior demonstrates what the old buggy code would have done.
// With the old code, since DependsOn was always empty from bd list,
// ALL open steps would appear "ready" regardless of actual dependencies.
// This test verifies the bug exists when using the old approach.
func TestOldBuggyBehavior(t *testing.T) {
m := newMockBeadsForStep()
// Setup: Step 2 depends on Step 1, but Step 1 is NOT closed
m.addIssue(makeStepIssue("gt-mol.1", "Step 1", "gt-mol", "open", nil))
m.addIssue(makeStepIssue("gt-mol.2", "Step 2", "gt-mol", "open", []string{"gt-mol.1"}))
// Get children via List (simulates bd list - DependsOn is empty)
children, _ := m.List(beads.ListOptions{Parent: "gt-mol", Status: "all"})
// OLD BUGGY CODE: Check DependsOn (which is empty from bd list)
closedIDs := make(map[string]bool)
var openSteps []*beads.Issue
for _, child := range children {
if child.Status == "closed" {
closedIDs[child.ID] = true
} else if child.Status == "open" {
openSteps = append(openSteps, child)
}
}
// Count how many steps the OLD buggy code thinks are "ready"
readyCount := 0
for _, step := range openSteps {
allDepsClosed := true
for _, depID := range step.DependsOn { // BUG: This is always empty!
if !closedIDs[depID] {
allDepsClosed = false
break
}
}
if len(step.DependsOn) == 0 || allDepsClosed { // Always true since DependsOn is empty
readyCount++
}
}
// The bug: OLD code thinks BOTH steps are ready (2 ready)
// Correct behavior: Only Step 1 should be ready (1 ready)
if readyCount != 2 {
t.Errorf("Expected old buggy code to mark 2 steps as ready, got %d", readyCount)
}
t.Log("Old buggy behavior confirmed: both steps marked ready when only step 1 should be")
}