From 672377544b2770ecbcb3d3492b879a0493b76e66 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 21 Nov 2025 16:11:29 -0500 Subject: [PATCH] test: Refactor compact_test.go to use shared DB pattern (bd-1rh) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of Phase 2/3 test suite optimization. BEFORE: - 10 separate test functions - Each creating its own database - 6 tests with DB setup overhead - Total: 10 test executions AFTER: - 1 TestCompactSuite with 6 shared-DB subtests - 4 standalone tests (no DB needed) - Single DB setup for suite - Total: 5 test functions IMPACT: - Lines: -135 (-22.4%) - DB setups: 6 → 1 (6x reduction) - Tests passing: 10/10 ✓ - Runtime: ~0.33s The suite consolidates all DB-dependent tests: - DryRun: eligibility check on closed issue - Stats: mix of eligible/ineligible issues - RunCompactStats: tests both normal and JSON output - CompactStatsJSON: JSON formatting path - RunCompactSingleDryRun: single issue eligibility - RunCompactAllDryRun: multiple issue eligibility Standalone tests (no DB): - TestCompactValidation: flag validation logic - TestCompactProgressBar: progress bar formatting - TestFormatUptime: uptime display formatting - TestCompactInitCommand: command initialization 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- cmd/bd/TEST_SUITE_AUDIT.md | 2 +- cmd/bd/compact_test.go | 601 ++++++++++++++----------------------- 2 files changed, 234 insertions(+), 369 deletions(-) diff --git a/cmd/bd/TEST_SUITE_AUDIT.md b/cmd/bd/TEST_SUITE_AUDIT.md index ffeec41a..349c0fa8 100644 --- a/cmd/bd/TEST_SUITE_AUDIT.md +++ b/cmd/bd/TEST_SUITE_AUDIT.md @@ -154,7 +154,7 @@ Tests that already use good patterns: **Recommendation**: Mix - some can share, some need isolation #### Misc Tests: -- **compact_test.go** (10 tests) +- ✅ **compact_test.go** (10 tests → 1 suite + 4 standalone = Phase 2 DONE) - **duplicates_test.go** (5 tests) - **epic_test.go** (3 tests) - **hooks_test.go** (6 tests) diff --git a/cmd/bd/compact_test.go b/cmd/bd/compact_test.go index b7633ab9..6a68ee29 100644 --- a/cmd/bd/compact_test.go +++ b/cmd/bd/compact_test.go @@ -3,74 +3,239 @@ package main import ( "context" "fmt" - "os" "path/filepath" "testing" "time" - "github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/types" ) -func TestCompactDryRun(t *testing.T) { +func TestCompactSuite(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(context.Background(), dbPath) - if err != nil { - t.Fatal(err) - } - defer sqliteStore.Close() - + testDB := filepath.Join(tmpDir, ".beads", "beads.db") + s := newTestStore(t, testDB) ctx := context.Background() - - // Set issue_prefix to prevent "database not initialized" errors - if err := sqliteStore.SetConfig(ctx, "issue_prefix", "test"); err != nil { - t.Fatalf("Failed to set issue_prefix: %v", err) - } - - // Create a closed issue - issue := &types.Issue{ - ID: "test-1", - Title: "Test Issue", - Description: "This is a long description that should be compacted. " + 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) - } + t.Run("DryRun", func(t *testing.T) { + // Create a closed issue + issue := &types.Issue{ + ID: "test-dryrun-1", + Title: "Test Issue", + Description: "This is a long description that should be compacted. " + 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)), + } - // Test dry run - should not error even without API key - compactDryRun = true - compactTier = 1 - compactID = "test-1" - compactForce = false - jsonOutput = false - - store = sqliteStore - daemonClient = nil + if err := s.CreateIssue(ctx, issue, "test"); err != nil { + t.Fatal(err) + } - // Should check eligibility without error - eligible, reason, err := sqliteStore.CheckEligibility(ctx, "test-1", 1) - if err != nil { - t.Fatalf("CheckEligibility failed: %v", err) - } - - if !eligible { - t.Fatalf("Issue should be eligible for compaction: %s", reason) - } + // Test dry run - should check eligibility without error even without API key + eligible, reason, err := s.CheckEligibility(ctx, "test-dryrun-1", 1) + if err != nil { + t.Fatalf("CheckEligibility failed: %v", err) + } - compactDryRun = false - compactID = "" + if !eligible { + t.Fatalf("Issue should be eligible for compaction: %s", reason) + } + }) + + t.Run("Stats", func(t *testing.T) { + // Create mix of issues - some eligible, some not + issues := []*types.Issue{ + { + ID: "test-stats-1", + Title: "Old closed", + 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)), + }, + { + ID: "test-stats-2", + Title: "Recent closed", + Status: types.StatusClosed, + Priority: 2, + IssueType: types.TypeTask, + CreatedAt: time.Now().Add(-10 * 24 * time.Hour), + ClosedAt: ptrTime(time.Now().Add(-5 * 24 * time.Hour)), + }, + { + ID: "test-stats-3", + Title: "Still open", + Status: types.StatusOpen, + Priority: 2, + IssueType: types.TypeTask, + CreatedAt: time.Now().Add(-40 * 24 * time.Hour), + }, + } + + for _, issue := range issues { + if err := s.CreateIssue(ctx, issue, "test"); err != nil { + t.Fatal(err) + } + } + + // Verify issues were created + allIssues, err := s.SearchIssues(ctx, "", types.IssueFilter{}) + if err != nil { + t.Fatalf("SearchIssues failed: %v", err) + } + + // Count issues with stats prefix + statCount := 0 + for _, issue := range allIssues { + if len(issue.ID) >= 11 && issue.ID[:11] == "test-stats-" { + statCount++ + } + } + + if statCount != 3 { + t.Errorf("Expected 3 stats issues, got %d", statCount) + } + + // Test eligibility check for old closed issue + eligible, _, err := s.CheckEligibility(ctx, "test-stats-1", 1) + if err != nil { + t.Fatalf("CheckEligibility failed: %v", err) + } + if !eligible { + t.Error("Old closed issue should be eligible for Tier 1") + } + }) + + t.Run("RunCompactStats", func(t *testing.T) { + // Create some closed issues + for i := 1; i <= 3; i++ { + id := fmt.Sprintf("test-runstats-%d", i) + issue := &types.Issue{ + ID: id, + 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 := s.CreateIssue(ctx, issue, "test"); err != nil { + t.Fatal(err) + } + } + + // Test stats - should work without API key + savedJSONOutput := jsonOutput + jsonOutput = false + defer func() { jsonOutput = savedJSONOutput }() + + // Actually call runCompactStats to increase coverage + runCompactStats(ctx, s) + + // Also test with JSON output + jsonOutput = true + runCompactStats(ctx, s) + }) + + t.Run("CompactStatsJSON", func(t *testing.T) { + // Create a closed issue eligible for Tier 1 + issue := &types.Issue{ + ID: "test-json-1", + 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 := s.CreateIssue(ctx, issue, "test"); err != nil { + t.Fatal(err) + } + + // Test with JSON output + savedJSONOutput := jsonOutput + jsonOutput = true + defer func() { jsonOutput = savedJSONOutput }() + + // Should not panic and should execute JSON path + runCompactStats(ctx, s) + }) + + t.Run("RunCompactSingleDryRun", func(t *testing.T) { + // Create a closed issue eligible for compaction + issue := &types.Issue{ + ID: "test-single-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 := s.CreateIssue(ctx, issue, "test"); err != nil { + t.Fatal(err) + } + + // Test eligibility in dry run mode + eligible, _, err := s.CheckEligibility(ctx, "test-single-1", 1) + if err != nil { + t.Fatalf("CheckEligibility failed: %v", err) + } + if !eligible { + t.Error("Issue should be eligible for Tier 1 compaction") + } + }) + + t.Run("RunCompactAllDryRun", func(t *testing.T) { + // 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 := s.CreateIssue(ctx, issue, "test"); err != nil { + t.Fatal(err) + } + } + + // Verify issues eligible for compaction + closedStatus := types.StatusClosed + issues, err := s.SearchIssues(ctx, "", types.IssueFilter{Status: &closedStatus}) + if err != nil { + t.Fatalf("SearchIssues failed: %v", err) + } + + eligibleCount := 0 + for _, issue := range issues { + // Only count our test-all issues + if len(issue.ID) < 9 || issue.ID[:9] != "test-all-" { + continue + } + eligible, _, err := s.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) + } + }) } func TestCompactValidation(t *testing.T) { @@ -83,10 +248,10 @@ func TestCompactValidation(t *testing.T) { wantError bool }{ { - name: "both id and all", - compactID: "test-1", + name: "both id and all", + compactID: "test-1", compactAll: true, - wantError: true, + wantError: true, }, { name: "force without id", @@ -103,9 +268,9 @@ func TestCompactValidation(t *testing.T) { wantError: false, }, { - name: "id only", - compactID: "test-1", - wantError: false, + name: "id only", + compactID: "test-1", + wantError: false, }, { name: "all only", @@ -122,14 +287,14 @@ func TestCompactValidation(t *testing.T) { t.Error("Expected error for both --id and --all") } } - + if tt.force && tt.compactID == "" { // Should fail if !tt.wantError { t.Error("Expected error for --force without --id") } } - + if tt.compactID == "" && !tt.compactAll && !tt.dryRun { // Should fail if !tt.wantError { @@ -140,147 +305,18 @@ func TestCompactValidation(t *testing.T) { } } -func TestCompactStats(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(context.Background(), dbPath) - if err != nil { - t.Fatal(err) - } - defer sqliteStore.Close() - - ctx := context.Background() - - // Set issue_prefix to prevent "database not initialized" errors - if err := sqliteStore.SetConfig(ctx, "issue_prefix", "test"); err != nil { - t.Fatalf("Failed to set issue_prefix: %v", err) - } - - // Create mix of issues - some eligible, some not - issues := []*types.Issue{ - { - ID: "test-1", - Title: "Old closed", - 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)), - }, - { - ID: "test-2", - Title: "Recent closed", - Status: types.StatusClosed, - Priority: 2, - IssueType: types.TypeTask, - CreatedAt: time.Now().Add(-10 * 24 * time.Hour), - ClosedAt: ptrTime(time.Now().Add(-5 * 24 * time.Hour)), - }, - { - ID: "test-3", - Title: "Still open", - Status: types.StatusOpen, - Priority: 2, - IssueType: types.TypeTask, - CreatedAt: time.Now().Add(-40 * 24 * time.Hour), - }, - } - - for _, issue := range issues { - if err := sqliteStore.CreateIssue(ctx, issue, "test"); err != nil { - t.Fatal(err) - } - } - - // Verify issues were created - allIssues, err := sqliteStore.SearchIssues(ctx, "", types.IssueFilter{}) - if err != nil { - t.Fatalf("SearchIssues failed: %v", err) - } - - if len(allIssues) != 3 { - t.Errorf("Expected 3 total issues, got %d", len(allIssues)) - } - - // Test eligibility check for old closed issue - eligible, _, err := sqliteStore.CheckEligibility(ctx, "test-1", 1) - if err != nil { - t.Fatalf("CheckEligibility failed: %v", err) - } - if !eligible { - t.Error("Old closed issue should be eligible for Tier 1") - } -} - -func TestRunCompactStats(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(context.Background(), 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 some closed issues - for i := 1; i <= 3; i++ { - id := "test-" + string(rune('0'+i)) - issue := &types.Issue{ - ID: id, - 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) - } - } - - // Test stats - should work without API key - savedJSONOutput := jsonOutput - jsonOutput = false - defer func() { jsonOutput = savedJSONOutput }() - - // Actually call runCompactStats to increase coverage - runCompactStats(ctx, sqliteStore) - - // Also test with JSON output - jsonOutput = true - runCompactStats(ctx, sqliteStore) -} - func TestCompactProgressBar(t *testing.T) { // Test progress bar formatting pb := progressBar(50, 100) if len(pb) == 0 { t.Error("Progress bar should not be empty") } - + pb = progressBar(100, 100) if len(pb) == 0 { t.Error("Full progress bar should not be empty") } - + pb = progressBar(0, 100) if len(pb) == 0 { t.Error("Zero progress bar should not be empty") @@ -333,189 +369,18 @@ func TestCompactInitCommand(t *testing.T) { if compactCmd == nil { t.Fatal("compactCmd should be initialized") } - + if compactCmd.Use != "compact" { t.Errorf("Expected Use='compact', got %q", compactCmd.Use) } - + if len(compactCmd.Long) == 0 { t.Error("compactCmd should have Long description") } - + // Verify --json flag exists jsonFlag := compactCmd.Flags().Lookup("json") if jsonFlag == nil { t.Error("compact command should have --json flag") } } - -func TestCompactStatsJSON(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(context.Background(), 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 Tier 1 - issue := &types.Issue{ - ID: "test-1", - 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) - } - - // Test with JSON output - savedJSONOutput := jsonOutput - jsonOutput = true - defer func() { jsonOutput = savedJSONOutput }() - - // 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(context.Background(), 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(context.Background(), 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) - } -}