diff --git a/cmd/bd/mol_test.go b/cmd/bd/mol_test.go index a8d18728..4c84fd8a 100644 --- a/cmd/bd/mol_test.go +++ b/cmd/bd/mol_test.go @@ -2364,3 +2364,166 @@ func TestCalculateBlockingDepths(t *testing.T) { t.Errorf("step3 depth = %d, want 3", depths["step3"]) } } + +// TestSpawnMoleculeEphemeralFlag verifies that spawnMolecule with ephemeral=true +// creates issues with the Ephemeral flag set (bd-phin) +func TestSpawnMoleculeEphemeralFlag(t *testing.T) { + ctx := context.Background() + dbPath := t.TempDir() + "/test.db" + s, err := sqlite.New(ctx, dbPath) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + defer s.Close() + if err := s.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set config: %v", err) + } + + // Create a template with a child (IDs will be auto-generated) + root := &types.Issue{ + Title: "Template Epic", + Status: types.StatusOpen, + Priority: 2, + IssueType: types.TypeEpic, + Labels: []string{MoleculeLabel}, // Required for loadTemplateSubgraph + } + child := &types.Issue{ + Title: "Template Task", + Status: types.StatusOpen, + Priority: 2, + IssueType: types.TypeTask, + } + + if err := s.CreateIssue(ctx, root, "test"); err != nil { + t.Fatalf("Failed to create template root: %v", err) + } + if err := s.CreateIssue(ctx, child, "test"); err != nil { + t.Fatalf("Failed to create template child: %v", err) + } + + // Add parent-child dependency + if err := s.AddDependency(ctx, &types.Dependency{ + IssueID: child.ID, + DependsOnID: root.ID, + Type: types.DepParentChild, + }, "test"); err != nil { + t.Fatalf("Failed to add parent-child dependency: %v", err) + } + + // Load subgraph + subgraph, err := loadTemplateSubgraph(ctx, s, root.ID) + if err != nil { + t.Fatalf("Failed to load subgraph: %v", err) + } + + // Spawn with ephemeral=true + result, err := spawnMolecule(ctx, s, subgraph, nil, "", "test", true, "eph") + if err != nil { + t.Fatalf("spawnMolecule failed: %v", err) + } + + // Verify all spawned issues have Ephemeral=true + for oldID, newID := range result.IDMapping { + spawned, err := s.GetIssue(ctx, newID) + if err != nil { + t.Fatalf("Failed to get spawned issue %s: %v", newID, err) + } + if !spawned.Ephemeral { + t.Errorf("Spawned issue %s (from %s) should have Ephemeral=true, got false", newID, oldID) + } + } + + // Verify spawned issues have the correct prefix + for _, newID := range result.IDMapping { + if !strings.HasPrefix(newID, "test-eph-") { + t.Errorf("Spawned issue ID %s should have prefix 'test-eph-'", newID) + } + } +} + +// TestSpawnMoleculeFromFormulaEphemeral verifies that spawning from a cooked formula +// with ephemeral=true creates issues with the Ephemeral flag set (bd-phin) +func TestSpawnMoleculeFromFormulaEphemeral(t *testing.T) { + ctx := context.Background() + dbPath := t.TempDir() + "/test.db" + s, err := sqlite.New(ctx, dbPath) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + defer s.Close() + if err := s.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set config: %v", err) + } + + // Create a minimal in-memory subgraph (simulating cookFormulaToSubgraph output) + root := &types.Issue{ + ID: "test-formula", + Title: "Test Formula", + Status: types.StatusOpen, + Priority: 2, + IssueType: types.TypeEpic, + IsTemplate: true, + } + step := &types.Issue{ + ID: "test-formula.step1", + Title: "Step 1", + Status: types.StatusOpen, + Priority: 2, + IssueType: types.TypeTask, + IsTemplate: true, + } + + subgraph := &TemplateSubgraph{ + Root: root, + Issues: []*types.Issue{root, step}, + Dependencies: []*types.Dependency{ + { + IssueID: step.ID, + DependsOnID: root.ID, + Type: types.DepParentChild, + }, + }, + IssueMap: map[string]*types.Issue{ + root.ID: root, + step.ID: step, + }, + } + + // Spawn with ephemeral=true (simulating bd mol wisp ) + result, err := spawnMolecule(ctx, s, subgraph, nil, "", "test", true, "eph") + if err != nil { + t.Fatalf("spawnMolecule failed: %v", err) + } + + // Verify all spawned issues have Ephemeral=true + for oldID, newID := range result.IDMapping { + spawned, err := s.GetIssue(ctx, newID) + if err != nil { + t.Fatalf("Failed to get spawned issue %s: %v", newID, err) + } + if !spawned.Ephemeral { + t.Errorf("Spawned issue %s (from %s) should have Ephemeral=true, got false", newID, oldID) + } + t.Logf("Issue %s: Ephemeral=%v", newID, spawned.Ephemeral) + } + + // Verify they have the correct prefix + for _, newID := range result.IDMapping { + if !strings.HasPrefix(newID, "test-eph-") { + t.Errorf("Spawned issue ID %s should have prefix 'test-eph-'", newID) + } + } + + // Verify ephemeral issues are excluded from ready work + readyWork, err := s.GetReadyWork(ctx, types.WorkFilter{}) + if err != nil { + t.Fatalf("GetReadyWork failed: %v", err) + } + for _, issue := range readyWork { + for _, spawnedID := range result.IDMapping { + if issue.ID == spawnedID { + t.Errorf("Ephemeral issue %s should not appear in ready work", spawnedID) + } + } + } +}