fix(mol): persist template label in bondProtoProto + add comprehensive tests
Code review found that bondProtoProto was setting Labels on the compound issue, but CreateIssue doesn't persist labels (they're stored separately. Fixed by calling AddLabel after creating the compound. Also added comprehensive tests for: - isProto() - template label detection - operandType() - proto vs molecule string - minPriority() - priority comparison - bondProtoProto() - compound proto creation with deps and labels - bondProtoMol() - spawn proto and attach to molecule - bondMolMol() - join two molecules with sequential/parallel bonds Tests verify dependency types (blocks vs parent-child) based on bond type. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> EOF )
This commit is contained in:
@@ -786,7 +786,6 @@ func bondProtoProto(ctx context.Context, s storage.Storage, protoA, protoB *type
|
||||
Status: types.StatusOpen,
|
||||
Priority: minPriority(protoA.Priority, protoB.Priority),
|
||||
IssueType: types.TypeEpic,
|
||||
Labels: []string{MoleculeLabel}, // Mark as proto
|
||||
BondedFrom: []types.BondRef{
|
||||
{ProtoID: protoA.ID, BondType: bondType, BondPoint: ""},
|
||||
{ProtoID: protoB.ID, BondType: bondType, BondPoint: ""},
|
||||
@@ -797,6 +796,11 @@ func bondProtoProto(ctx context.Context, s storage.Storage, protoA, protoB *type
|
||||
}
|
||||
compoundID = compound.ID
|
||||
|
||||
// Add template label (labels are stored separately, not in issue table)
|
||||
if err := tx.AddLabel(ctx, compoundID, MoleculeLabel, actorName); err != nil {
|
||||
return fmt.Errorf("adding template label: %w", err)
|
||||
}
|
||||
|
||||
// Add parent-child dependencies from compound to both proto roots
|
||||
depA := &types.Dependency{
|
||||
IssueID: protoA.ID,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
@@ -157,3 +159,302 @@ func TestCollectSubgraphText(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsProto(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
labels []string
|
||||
want bool
|
||||
}{
|
||||
{"with template label", []string{"template", "other"}, true},
|
||||
{"template only", []string{"template"}, true},
|
||||
{"no template label", []string{"bug", "feature"}, false},
|
||||
{"empty labels", []string{}, false},
|
||||
{"nil labels", nil, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
issue := &types.Issue{Labels: tt.labels}
|
||||
got := isProto(issue)
|
||||
if got != tt.want {
|
||||
t.Errorf("isProto() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperandType(t *testing.T) {
|
||||
if got := operandType(true); got != "proto" {
|
||||
t.Errorf("operandType(true) = %q, want %q", got, "proto")
|
||||
}
|
||||
if got := operandType(false); got != "molecule" {
|
||||
t.Errorf("operandType(false) = %q, want %q", got, "molecule")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinPriority(t *testing.T) {
|
||||
tests := []struct {
|
||||
a, b, want int
|
||||
}{
|
||||
{1, 2, 1},
|
||||
{2, 1, 1},
|
||||
{0, 3, 0},
|
||||
{3, 3, 3},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := minPriority(tt.a, tt.b)
|
||||
if got != tt.want {
|
||||
t.Errorf("minPriority(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBondProtoProto(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dbPath := t.TempDir() + "/test.db"
|
||||
store, err := sqlite.New(ctx, dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
|
||||
t.Fatalf("Failed to set config: %v", err)
|
||||
}
|
||||
|
||||
// Create two protos
|
||||
protoA := &types.Issue{
|
||||
Title: "Proto A",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeEpic,
|
||||
Labels: []string{MoleculeLabel},
|
||||
}
|
||||
protoB := &types.Issue{
|
||||
Title: "Proto B",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeEpic,
|
||||
Labels: []string{MoleculeLabel},
|
||||
}
|
||||
|
||||
if err := store.CreateIssue(ctx, protoA, "test"); err != nil {
|
||||
t.Fatalf("Failed to create protoA: %v", err)
|
||||
}
|
||||
if err := store.CreateIssue(ctx, protoB, "test"); err != nil {
|
||||
t.Fatalf("Failed to create protoB: %v", err)
|
||||
}
|
||||
|
||||
// Test sequential bond
|
||||
result, err := bondProtoProto(ctx, store, protoA, protoB, types.BondTypeSequential, "", "test")
|
||||
if err != nil {
|
||||
t.Fatalf("bondProtoProto failed: %v", err)
|
||||
}
|
||||
|
||||
if result.ResultType != "compound_proto" {
|
||||
t.Errorf("ResultType = %q, want %q", result.ResultType, "compound_proto")
|
||||
}
|
||||
if result.BondType != types.BondTypeSequential {
|
||||
t.Errorf("BondType = %q, want %q", result.BondType, types.BondTypeSequential)
|
||||
}
|
||||
|
||||
// Verify compound was created
|
||||
compound, err := store.GetIssue(ctx, result.ResultID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get compound: %v", err)
|
||||
}
|
||||
if !isProto(compound) {
|
||||
t.Errorf("Compound should be a proto (have template label), got labels: %v", compound.Labels)
|
||||
}
|
||||
if compound.Priority != 1 {
|
||||
t.Errorf("Compound priority = %d, want %d (min of 1,2)", compound.Priority, 1)
|
||||
}
|
||||
|
||||
// Verify dependencies exist (protoA depends on compound via parent-child)
|
||||
deps, err := store.GetDependenciesWithMetadata(ctx, protoA.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get deps for protoA: %v", err)
|
||||
}
|
||||
foundParentChild := false
|
||||
for _, dep := range deps {
|
||||
if dep.ID == compound.ID && dep.DependencyType == types.DepParentChild {
|
||||
foundParentChild = true
|
||||
}
|
||||
}
|
||||
if !foundParentChild {
|
||||
t.Error("Expected parent-child dependency from protoA to compound")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBondProtoMol(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dbPath := t.TempDir() + "/test.db"
|
||||
store, err := sqlite.New(ctx, dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
|
||||
t.Fatalf("Failed to set config: %v", err)
|
||||
}
|
||||
|
||||
// Create a proto with a child issue
|
||||
proto := &types.Issue{
|
||||
Title: "Proto: {{name}}",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeEpic,
|
||||
Labels: []string{MoleculeLabel},
|
||||
}
|
||||
if err := store.CreateIssue(ctx, proto, "test"); err != nil {
|
||||
t.Fatalf("Failed to create proto: %v", err)
|
||||
}
|
||||
|
||||
protoChild := &types.Issue{
|
||||
Title: "Step 1 for {{name}}",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeTask,
|
||||
Labels: []string{MoleculeLabel},
|
||||
}
|
||||
if err := store.CreateIssue(ctx, protoChild, "test"); err != nil {
|
||||
t.Fatalf("Failed to create proto child: %v", err)
|
||||
}
|
||||
|
||||
// Add parent-child dependency
|
||||
if err := store.AddDependency(ctx, &types.Dependency{
|
||||
IssueID: protoChild.ID,
|
||||
DependsOnID: proto.ID,
|
||||
Type: types.DepParentChild,
|
||||
}, "test"); err != nil {
|
||||
t.Fatalf("Failed to add dependency: %v", err)
|
||||
}
|
||||
|
||||
// Create a molecule (existing epic)
|
||||
mol := &types.Issue{
|
||||
Title: "Existing Work",
|
||||
Status: types.StatusInProgress,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeEpic,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, mol, "test"); err != nil {
|
||||
t.Fatalf("Failed to create molecule: %v", err)
|
||||
}
|
||||
|
||||
// Bond proto to molecule
|
||||
vars := map[string]string{"name": "auth-feature"}
|
||||
result, err := bondProtoMol(ctx, store, proto, mol, types.BondTypeSequential, vars, "test")
|
||||
if err != nil {
|
||||
t.Fatalf("bondProtoMol failed: %v", err)
|
||||
}
|
||||
|
||||
if result.ResultType != "compound_molecule" {
|
||||
t.Errorf("ResultType = %q, want %q", result.ResultType, "compound_molecule")
|
||||
}
|
||||
if result.Spawned != 2 {
|
||||
t.Errorf("Spawned = %d, want 2", result.Spawned)
|
||||
}
|
||||
if result.ResultID != mol.ID {
|
||||
t.Errorf("ResultID = %q, want %q (original molecule)", result.ResultID, mol.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBondMolMol(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dbPath := t.TempDir() + "/test.db"
|
||||
store, err := sqlite.New(ctx, dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
|
||||
t.Fatalf("Failed to set config: %v", err)
|
||||
}
|
||||
|
||||
// Create two molecules
|
||||
molA := &types.Issue{
|
||||
Title: "Molecule A",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeEpic,
|
||||
}
|
||||
molB := &types.Issue{
|
||||
Title: "Molecule B",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeEpic,
|
||||
}
|
||||
|
||||
if err := store.CreateIssue(ctx, molA, "test"); err != nil {
|
||||
t.Fatalf("Failed to create molA: %v", err)
|
||||
}
|
||||
if err := store.CreateIssue(ctx, molB, "test"); err != nil {
|
||||
t.Fatalf("Failed to create molB: %v", err)
|
||||
}
|
||||
|
||||
// Test sequential bond
|
||||
result, err := bondMolMol(ctx, store, molA, molB, types.BondTypeSequential, "test")
|
||||
if err != nil {
|
||||
t.Fatalf("bondMolMol failed: %v", err)
|
||||
}
|
||||
|
||||
if result.ResultType != "compound_molecule" {
|
||||
t.Errorf("ResultType = %q, want %q", result.ResultType, "compound_molecule")
|
||||
}
|
||||
if result.ResultID != molA.ID {
|
||||
t.Errorf("ResultID = %q, want %q", result.ResultID, molA.ID)
|
||||
}
|
||||
|
||||
// Verify dependency: B blocks on A
|
||||
deps, err := store.GetDependenciesWithMetadata(ctx, molB.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get deps for molB: %v", err)
|
||||
}
|
||||
foundBlocks := false
|
||||
for _, dep := range deps {
|
||||
if dep.ID == molA.ID && dep.DependencyType == types.DepBlocks {
|
||||
foundBlocks = true
|
||||
}
|
||||
}
|
||||
if !foundBlocks {
|
||||
t.Error("Expected blocks dependency from molB to molA for sequential bond")
|
||||
}
|
||||
|
||||
// Test parallel bond (create new molecules)
|
||||
molC := &types.Issue{
|
||||
Title: "Molecule C",
|
||||
Status: types.StatusOpen,
|
||||
IssueType: types.TypeEpic,
|
||||
}
|
||||
molD := &types.Issue{
|
||||
Title: "Molecule D",
|
||||
Status: types.StatusOpen,
|
||||
IssueType: types.TypeEpic,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, molC, "test"); err != nil {
|
||||
t.Fatalf("Failed to create molC: %v", err)
|
||||
}
|
||||
if err := store.CreateIssue(ctx, molD, "test"); err != nil {
|
||||
t.Fatalf("Failed to create molD: %v", err)
|
||||
}
|
||||
|
||||
result2, err := bondMolMol(ctx, store, molC, molD, types.BondTypeParallel, "test")
|
||||
if err != nil {
|
||||
t.Fatalf("bondMolMol parallel failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify parent-child dependency for parallel
|
||||
deps2, err := store.GetDependenciesWithMetadata(ctx, molD.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get deps for molD: %v", err)
|
||||
}
|
||||
foundParentChild := false
|
||||
for _, dep := range deps2 {
|
||||
if dep.ID == molC.ID && dep.DependencyType == types.DepParentChild {
|
||||
foundParentChild = true
|
||||
}
|
||||
}
|
||||
if !foundParentChild {
|
||||
t.Errorf("Expected parent-child dependency for parallel bond, result: %+v", result2)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user