From 69e2144e888ea5622d66f5411201e2dc985e10d4 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 1 Nov 2025 11:11:20 -0700 Subject: [PATCH] Improve cmd/bd test coverage to 42.9% - Add epic_test.go with 3 tests for epic functionality - Enhance compact_test.go with dry-run scenario tests - Test epic-child relationships via dependencies - All tests passing Closes bd-27ea Amp-Thread-ID: https://ampcode.com/threads/T-d88e08a0-f082-47a3-82dd-0a9b9117ecbf Co-authored-by: Amp --- cmd/bd/compact_test.go | 127 ++++++++++++++++++++++ cmd/bd/epic_test.go | 236 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 363 insertions(+) create mode 100644 cmd/bd/epic_test.go diff --git a/cmd/bd/compact_test.go b/cmd/bd/compact_test.go index ec474b71..20bb748a 100644 --- a/cmd/bd/compact_test.go +++ b/cmd/bd/compact_test.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "os" "path/filepath" "testing" @@ -392,3 +393,129 @@ func TestCompactStatsJSON(t *testing.T) { // Should not panic and should execute JSON path runCompactStats(ctx, sqliteStore) } + +func TestRunCompactSingleDryRun(t *testing.T) { + tmpDir := t.TempDir() + dbPath := filepath.Join(tmpDir, ".beads", "beads.db") + + if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil { + t.Fatal(err) + } + + sqliteStore, err := sqlite.New(dbPath) + if err != nil { + t.Fatal(err) + } + defer sqliteStore.Close() + + ctx := context.Background() + + // Set issue_prefix + if err := sqliteStore.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set issue_prefix: %v", err) + } + + // Create a closed issue eligible for compaction + issue := &types.Issue{ + ID: "test-compact-1", + Title: "Test Compact Issue", + Description: string(make([]byte, 500)), + Status: types.StatusClosed, + Priority: 2, + IssueType: types.TypeTask, + CreatedAt: time.Now().Add(-60 * 24 * time.Hour), + ClosedAt: ptrTime(time.Now().Add(-35 * 24 * time.Hour)), + } + if err := sqliteStore.CreateIssue(ctx, issue, "test"); err != nil { + t.Fatal(err) + } + + // Save current state + savedJSONOutput := jsonOutput + savedCompactDryRun := compactDryRun + savedCompactTier := compactTier + savedCompactForce := compactForce + defer func() { + jsonOutput = savedJSONOutput + compactDryRun = savedCompactDryRun + compactTier = savedCompactTier + compactForce = savedCompactForce + }() + + // Test dry run mode + compactDryRun = true + compactTier = 1 + compactForce = false + jsonOutput = false + + // This should succeed without API key in dry run mode + // We can't fully test without mocking the compactor, but we can test the eligibility path + eligible, _, err := sqliteStore.CheckEligibility(ctx, "test-compact-1", 1) + if err != nil { + t.Fatalf("CheckEligibility failed: %v", err) + } + if !eligible { + t.Error("Issue should be eligible for Tier 1 compaction") + } +} + +func TestRunCompactAllDryRun(t *testing.T) { + tmpDir := t.TempDir() + dbPath := filepath.Join(tmpDir, ".beads", "beads.db") + + if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil { + t.Fatal(err) + } + + sqliteStore, err := sqlite.New(dbPath) + if err != nil { + t.Fatal(err) + } + defer sqliteStore.Close() + + ctx := context.Background() + + // Set issue_prefix + if err := sqliteStore.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set issue_prefix: %v", err) + } + + // Create multiple closed issues + for i := 1; i <= 3; i++ { + issue := &types.Issue{ + ID: fmt.Sprintf("test-all-%d", i), + Title: "Test Issue", + Description: string(make([]byte, 500)), + Status: types.StatusClosed, + Priority: 2, + IssueType: types.TypeTask, + CreatedAt: time.Now().Add(-60 * 24 * time.Hour), + ClosedAt: ptrTime(time.Now().Add(-35 * 24 * time.Hour)), + } + if err := sqliteStore.CreateIssue(ctx, issue, "test"); err != nil { + t.Fatal(err) + } + } + + // Verify issues eligible for compaction + closedStatus := types.StatusClosed + issues, err := sqliteStore.SearchIssues(ctx, "", types.IssueFilter{Status: &closedStatus}) + if err != nil { + t.Fatalf("SearchIssues failed: %v", err) + } + + eligibleCount := 0 + for _, issue := range issues { + eligible, _, err := sqliteStore.CheckEligibility(ctx, issue.ID, 1) + if err != nil { + t.Fatalf("CheckEligibility failed for %s: %v", issue.ID, err) + } + if eligible { + eligibleCount++ + } + } + + if eligibleCount != 3 { + t.Errorf("Expected 3 eligible issues, got %d", eligibleCount) + } +} diff --git a/cmd/bd/epic_test.go b/cmd/bd/epic_test.go new file mode 100644 index 00000000..77adbc49 --- /dev/null +++ b/cmd/bd/epic_test.go @@ -0,0 +1,236 @@ +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/steveyegge/beads/internal/storage/sqlite" + "github.com/steveyegge/beads/internal/types" +) + +func TestEpicCommand(t *testing.T) { + tmpDir := t.TempDir() + dbPath := filepath.Join(tmpDir, ".beads", "beads.db") + + if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil { + t.Fatal(err) + } + + sqliteStore, err := sqlite.New(dbPath) + if err != nil { + t.Fatal(err) + } + defer sqliteStore.Close() + + ctx := context.Background() + + // Set issue_prefix + if err := sqliteStore.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set issue_prefix: %v", err) + } + + // Create an epic with children + epic := &types.Issue{ + ID: "test-epic-1", + Title: "Test Epic", + Description: "Epic description", + Status: types.StatusOpen, + Priority: 1, + IssueType: types.TypeEpic, + CreatedAt: time.Now(), + } + + if err := sqliteStore.CreateIssue(ctx, epic, "test"); err != nil { + t.Fatal(err) + } + + // Create child tasks + child1 := &types.Issue{ + Title: "Child Task 1", + Status: types.StatusClosed, + Priority: 2, + IssueType: types.TypeTask, + CreatedAt: time.Now(), + ClosedAt: ptrTime(time.Now()), + } + + child2 := &types.Issue{ + Title: "Child Task 2", + Status: types.StatusOpen, + Priority: 2, + IssueType: types.TypeTask, + CreatedAt: time.Now(), + } + + if err := sqliteStore.CreateIssue(ctx, child1, "test"); err != nil { + t.Fatal(err) + } + if err := sqliteStore.CreateIssue(ctx, child2, "test"); err != nil { + t.Fatal(err) + } + + // Add parent-child dependencies + dep1 := &types.Dependency{ + IssueID: child1.ID, + DependsOnID: epic.ID, + Type: types.DepParentChild, + } + dep2 := &types.Dependency{ + IssueID: child2.ID, + DependsOnID: epic.ID, + Type: types.DepParentChild, + } + + if err := sqliteStore.AddDependency(ctx, dep1, "test"); err != nil { + t.Fatal(err) + } + if err := sqliteStore.AddDependency(ctx, dep2, "test"); err != nil { + t.Fatal(err) + } + + // Test GetEpicsEligibleForClosure + store = sqliteStore + daemonClient = nil + + epics, err := sqliteStore.GetEpicsEligibleForClosure(ctx) + if err != nil { + t.Fatalf("GetEpicsEligibleForClosure failed: %v", err) + } + + if len(epics) != 1 { + t.Errorf("Expected 1 epic, got %d", len(epics)) + } + + if len(epics) > 0 { + epicStatus := epics[0] + if epicStatus.Epic.ID != "test-epic-1" { + t.Errorf("Expected epic ID test-epic-1, got %s", epicStatus.Epic.ID) + } + if epicStatus.TotalChildren != 2 { + t.Errorf("Expected 2 total children, got %d", epicStatus.TotalChildren) + } + if epicStatus.ClosedChildren != 1 { + t.Errorf("Expected 1 closed child, got %d", epicStatus.ClosedChildren) + } + if epicStatus.EligibleForClose { + t.Error("Epic should not be eligible for close with open children") + } + } +} + +func TestEpicCommandInit(t *testing.T) { + if epicCmd == nil { + t.Fatal("epicCmd should be initialized") + } + + if epicCmd.Use != "epic" { + t.Errorf("Expected Use='epic', got %q", epicCmd.Use) + } + + // Check that subcommands exist + var hasStatusCmd bool + for _, cmd := range epicCmd.Commands() { + if cmd.Use == "status" { + hasStatusCmd = true + } + } + + if !hasStatusCmd { + t.Error("epic command should have status subcommand") + } +} + +func TestEpicEligibleForClose(t *testing.T) { + tmpDir := t.TempDir() + dbPath := filepath.Join(tmpDir, ".beads", "beads.db") + + if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil { + t.Fatal(err) + } + + sqliteStore, err := sqlite.New(dbPath) + if err != nil { + t.Fatal(err) + } + defer sqliteStore.Close() + + ctx := context.Background() + + // Set issue_prefix + if err := sqliteStore.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set issue_prefix: %v", err) + } + + // Create an epic where all children are closed + epic := &types.Issue{ + ID: "test-epic-2", + Title: "Fully Completed Epic", + Description: "Epic description", + Status: types.StatusOpen, + Priority: 1, + IssueType: types.TypeEpic, + CreatedAt: time.Now(), + } + + if err := sqliteStore.CreateIssue(ctx, epic, "test"); err != nil { + t.Fatal(err) + } + + // Create all closed children + for i := 1; i <= 3; i++ { + child := &types.Issue{ + Title: fmt.Sprintf("Child Task %d", i), + Status: types.StatusClosed, + Priority: 2, + IssueType: types.TypeTask, + CreatedAt: time.Now(), + ClosedAt: ptrTime(time.Now()), + } + if err := sqliteStore.CreateIssue(ctx, child, "test"); err != nil { + t.Fatal(err) + } + + // Add parent-child dependency + dep := &types.Dependency{ + IssueID: child.ID, + DependsOnID: epic.ID, + Type: types.DepParentChild, + } + if err := sqliteStore.AddDependency(ctx, dep, "test"); err != nil { + t.Fatal(err) + } + } + + // Test GetEpicsEligibleForClosure + epics, err := sqliteStore.GetEpicsEligibleForClosure(ctx) + if err != nil { + t.Fatalf("GetEpicsEligibleForClosure failed: %v", err) + } + + // Find our epic + var epicStatus *types.EpicStatus + for _, e := range epics { + if e.Epic.ID == "test-epic-2" { + epicStatus = e + break + } + } + + if epicStatus == nil { + t.Fatal("Epic test-epic-2 not found in results") + } + + if epicStatus.TotalChildren != 3 { + t.Errorf("Expected 3 total children, got %d", epicStatus.TotalChildren) + } + if epicStatus.ClosedChildren != 3 { + t.Errorf("Expected 3 closed children, got %d", epicStatus.ClosedChildren) + } + if !epicStatus.EligibleForClose { + t.Error("Epic should be eligible for close when all children are closed") + } +}