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:
181
cmd/bd/autoflush_version_test.go
Normal file
181
cmd/bd/autoflush_version_test.go
Normal 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()
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
56
cmd/bd/daemons_test.go
Normal 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
|
||||
Reference in New Issue
Block a user