Improve test coverage for cmd/bd

- 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 <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-10-31 18:40:20 -07:00
parent 4fbff148f0
commit e313e6e2c2
4 changed files with 328 additions and 0 deletions

View File

@@ -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()
}

View File

@@ -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()

View File

@@ -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)

56
cmd/bd/daemons_test.go Normal file
View File

@@ -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