feat(beads): add cycle detection for molecule dependencies

Implement DFS-based cycle detection in ValidateMolecule to catch
circular dependencies in molecule step graphs. The algorithm uses
three-color marking (unvisited/visiting/visited) to detect back
edges that indicate cycles.

When a cycle is detected, the error message shows the cycle path
(e.g., "a -> b -> c -> a") for easy debugging.

Add 4 new tests:
- SimpleCycle: A -> B -> A
- LongerCycle: A -> B -> C -> A
- DiamondNoCycle: ensures valid diamond patterns pass
- CycleInSubgraph: cycle not involving root node

Closes gt-ai1z.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-21 21:49:07 -08:00
parent 887e2f25e4
commit 45ccce0f2b
2 changed files with 174 additions and 1 deletions

View File

@@ -2,6 +2,7 @@ package beads
import (
"reflect"
"strings"
"testing"
)
@@ -489,3 +490,108 @@ Has content.`
t.Errorf("step[1].Instructions = %q", steps[1].Instructions)
}
}
func TestValidateMolecule_SimpleCycle(t *testing.T) {
// A -> B -> A (simple 2-node cycle)
mol := &Issue{
ID: "mol-xyz",
Type: "molecule",
Description: `## Step: a
First step.
Needs: b
## Step: b
Second step.
Needs: a`,
}
err := ValidateMolecule(mol)
if err == nil {
t.Error("ValidateMolecule() = nil, want error for cycle")
}
if err != nil && !strings.Contains(err.Error(), "cycle") {
t.Errorf("error %q should mention 'cycle'", err.Error())
}
}
func TestValidateMolecule_LongerCycle(t *testing.T) {
// A -> B -> C -> A (3-node cycle)
mol := &Issue{
ID: "mol-xyz",
Type: "molecule",
Description: `## Step: a
First step.
Needs: c
## Step: b
Second step.
Needs: a
## Step: c
Third step.
Needs: b`,
}
err := ValidateMolecule(mol)
if err == nil {
t.Error("ValidateMolecule() = nil, want error for cycle")
}
if err != nil && !strings.Contains(err.Error(), "cycle") {
t.Errorf("error %q should mention 'cycle'", err.Error())
}
}
func TestValidateMolecule_DiamondNoCycle(t *testing.T) {
// Diamond pattern: A -> B, A -> C, B -> D, C -> D
// This has no cycle, should pass
mol := &Issue{
ID: "mol-xyz",
Type: "molecule",
Description: `## Step: a
Root step.
## Step: b
Branch 1.
Needs: a
## Step: c
Branch 2.
Needs: a
## Step: d
Merge point.
Needs: b, c`,
}
err := ValidateMolecule(mol)
if err != nil {
t.Errorf("ValidateMolecule() = %v, want nil (diamond has no cycle)", err)
}
}
func TestValidateMolecule_CycleInSubgraph(t *testing.T) {
// Root -> A, A -> B -> C -> A (cycle not involving root)
mol := &Issue{
ID: "mol-xyz",
Type: "molecule",
Description: `## Step: root
Starting point.
## Step: a
First in cycle.
Needs: root, c
## Step: b
Second in cycle.
Needs: a
## Step: c
Third in cycle.
Needs: b`,
}
err := ValidateMolecule(mol)
if err == nil {
t.Error("ValidateMolecule() = nil, want error for cycle in subgraph")
}
}