From e313e6e2c28994bf0de2bd7746e6ba5acab3ce74 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 31 Oct 2025 18:40:20 -0700 Subject: [PATCH] Improve test coverage for cmd/bd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comprehensive version mismatch tests (checkVersionMismatch: 25% → 100%) - Add daemon formatting helper tests (formatDaemonDuration, formatDaemonRelativeTime: 0% → 100%) - Add auto-import test cases (checkAndAutoImport: 27.8% → 44.4%) - Add compact eligibility test Overall coverage: 47.9% → 48.3% cmd/bd coverage: 20.5% → 21.0% New test files: - cmd/bd/autoflush_version_test.go (5 test functions) - cmd/bd/daemons_test.go (2 test functions) All tests passing. Amp-Thread-ID: https://ampcode.com/threads/T-29fa5379-fd71-4f75-bc4f-272beff96c8f Co-authored-by: Amp --- cmd/bd/autoflush_version_test.go | 181 +++++++++++++++++++++++++++++++ cmd/bd/autoimport_test.go | 35 ++++++ cmd/bd/compact_test.go | 56 ++++++++++ cmd/bd/daemons_test.go | 56 ++++++++++ 4 files changed, 328 insertions(+) create mode 100644 cmd/bd/autoflush_version_test.go create mode 100644 cmd/bd/daemons_test.go diff --git a/cmd/bd/autoflush_version_test.go b/cmd/bd/autoflush_version_test.go new file mode 100644 index 00000000..62534a21 --- /dev/null +++ b/cmd/bd/autoflush_version_test.go @@ -0,0 +1,181 @@ +package main + +import ( + "context" + "os" + "testing" + + "github.com/steveyegge/beads/internal/storage/sqlite" +) + +func TestCheckVersionMismatch_NoVersion(t *testing.T) { + tmpDir := t.TempDir() + tmpDB := tmpDir + "/test.db" + + sqliteStore, err := sqlite.New(tmpDB) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + defer sqliteStore.Close() + + ctx := context.Background() + + // Set prefix to initialize DB + if err := sqliteStore.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set prefix: %v", err) + } + + // Save and restore global store + oldStore := store + store = sqliteStore + defer func() { store = oldStore }() + + // Should not panic when no version is set + checkVersionMismatch() + + // Should have set the version + version, err := sqliteStore.GetMetadata(ctx, "bd_version") + if err != nil { + t.Fatalf("Failed to get version: %v", err) + } + + if version != Version { + t.Errorf("Expected version %s, got %s", Version, version) + } +} + +func TestCheckVersionMismatch_SameVersion(t *testing.T) { + tmpDir := t.TempDir() + tmpDB := tmpDir + "/test.db" + + sqliteStore, err := sqlite.New(tmpDB) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + defer sqliteStore.Close() + + ctx := context.Background() + + // Set prefix to initialize DB + if err := sqliteStore.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set prefix: %v", err) + } + + // Set same version + if err := sqliteStore.SetMetadata(ctx, "bd_version", Version); err != nil { + t.Fatalf("Failed to set version: %v", err) + } + + // Save and restore global store + oldStore := store + store = sqliteStore + defer func() { store = oldStore }() + + // Should not print warning (we can't easily test stderr, but ensure no panic) + checkVersionMismatch() +} + +func TestCheckVersionMismatch_OlderBinary(t *testing.T) { + tmpDir := t.TempDir() + tmpDB := tmpDir + "/test.db" + + sqliteStore, err := sqlite.New(tmpDB) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + defer sqliteStore.Close() + + ctx := context.Background() + + // Set prefix to initialize DB + if err := sqliteStore.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set prefix: %v", err) + } + + // Set a newer version in DB + if err := sqliteStore.SetMetadata(ctx, "bd_version", "99.99.99"); err != nil { + t.Fatalf("Failed to set version: %v", err) + } + + // Save and restore global store + oldStore := store + store = sqliteStore + defer func() { store = oldStore }() + + // Should print warning (we can't easily test stderr, but ensure no panic) + checkVersionMismatch() +} + +func TestCheckVersionMismatch_NewerBinary(t *testing.T) { + tmpDir := t.TempDir() + tmpDB := tmpDir + "/test.db" + + sqliteStore, err := sqlite.New(tmpDB) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + defer sqliteStore.Close() + + ctx := context.Background() + + // Set prefix to initialize DB + if err := sqliteStore.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set prefix: %v", err) + } + + // Set an older version in DB + if err := sqliteStore.SetMetadata(ctx, "bd_version", "0.1.0"); err != nil { + t.Fatalf("Failed to set version: %v", err) + } + + // Save and restore global store + oldStore := store + store = sqliteStore + defer func() { store = oldStore }() + + // Should print warning and update version + checkVersionMismatch() + + // Check that version was updated + version, err := sqliteStore.GetMetadata(ctx, "bd_version") + if err != nil { + t.Fatalf("Failed to get version: %v", err) + } + + if version != Version { + t.Errorf("Expected version to be updated to %s, got %s", Version, version) + } +} + +func TestCheckVersionMismatch_DebugMode(t *testing.T) { + tmpDir := t.TempDir() + tmpDB := tmpDir + "/test.db" + + sqliteStore, err := sqlite.New(tmpDB) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + defer sqliteStore.Close() + + ctx := context.Background() + + // Set prefix to initialize DB + if err := sqliteStore.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set prefix: %v", err) + } + + // Save and restore global store + oldStore := store + store = sqliteStore + defer func() { store = oldStore }() + + // Set debug mode + os.Setenv("BD_DEBUG", "1") + defer os.Unsetenv("BD_DEBUG") + + // Close the store to trigger metadata error + sqliteStore.Close() + + // Should not panic even with error in debug mode + checkVersionMismatch() +} diff --git a/cmd/bd/autoimport_test.go b/cmd/bd/autoimport_test.go index 09853ca5..af867e7a 100644 --- a/cmd/bd/autoimport_test.go +++ b/cmd/bd/autoimport_test.go @@ -67,6 +67,41 @@ func TestCheckAndAutoImport_DatabaseHasIssues(t *testing.T) { } } +func TestCheckAndAutoImport_EmptyDatabaseNoGit(t *testing.T) { + ctx := context.Background() + tmpDir := t.TempDir() + tmpDB := filepath.Join(tmpDir, "test.db") + store, err := sqlite.New(tmpDB) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + defer store.Close() + + // Set prefix + if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set prefix: %v", err) + } + + oldNoAutoImport := noAutoImport + oldJsonOutput := jsonOutput + noAutoImport = false + jsonOutput = true // Suppress output + defer func() { + noAutoImport = oldNoAutoImport + jsonOutput = oldJsonOutput + }() + + // Change to temp dir (no git repo) + oldWd, _ := os.Getwd() + defer os.Chdir(oldWd) + os.Chdir(tmpDir) + + result := checkAndAutoImport(ctx, store) + if result { + t.Error("Expected auto-import to skip when no git repo") + } +} + func TestFindBeadsDir(t *testing.T) { // Create temp directory with .beads tmpDir := t.TempDir() diff --git a/cmd/bd/compact_test.go b/cmd/bd/compact_test.go index dc58f0b5..ad6c4fba 100644 --- a/cmd/bd/compact_test.go +++ b/cmd/bd/compact_test.go @@ -216,6 +216,62 @@ func TestCompactStats(t *testing.T) { } } +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(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 }() + + // The function calls os.Exit, so we can't directly test it + // But we can test the eligibility checking which is the core logic + eligible, reason, err := sqliteStore.CheckEligibility(ctx, "test-1", 1) + if err != nil { + t.Fatalf("CheckEligibility failed: %v", err) + } + + if !eligible { + t.Logf("Not eligible: %s", reason) + } +} + func TestCompactProgressBar(t *testing.T) { // Test progress bar formatting pb := progressBar(50, 100) diff --git a/cmd/bd/daemons_test.go b/cmd/bd/daemons_test.go new file mode 100644 index 00000000..b38fc2ea --- /dev/null +++ b/cmd/bd/daemons_test.go @@ -0,0 +1,56 @@ +package main + +import ( + "testing" + "time" +) + +func TestFormatDaemonDuration(t *testing.T) { + tests := []struct { + name string + seconds float64 + expected string + }{ + {"zero", 0, "0s"}, + {"seconds", 45, "45s"}, + {"minutes", 90.5, "2m"}, + {"hours", 3661, "1.0h"}, + {"days", 86400, "1.0d"}, + {"mixed", 93784, "1.1d"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := formatDaemonDuration(tt.seconds) + if got != tt.expected { + t.Errorf("formatDaemonDuration(%f) = %q, want %q", tt.seconds, got, tt.expected) + } + }) + } +} + +func TestFormatDaemonRelativeTime(t *testing.T) { + tests := []struct { + name string + ago time.Duration + expected string + }{ + {"just now", 5 * time.Second, "just now"}, + {"minutes ago", 3 * time.Minute, "3m ago"}, + {"hours ago", 2 * time.Hour, "2.0h ago"}, + {"days ago", 25 * time.Hour, "1.0d ago"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testTime := time.Now().Add(-tt.ago) + got := formatDaemonRelativeTime(testTime) + if got != tt.expected { + t.Errorf("formatDaemonRelativeTime(%v) = %q, want %q", testTime, got, tt.expected) + } + }) + } +} + +// TestDaemonsFormatFunctions tests the formatting helpers +// Integration tests for the actual commands are in daemon_test.go