Files
beads/cmd/bd/completions_test.go
Dmitry Verkhoturov c1a9dda173 fix: allow shell completions to work without a database (#1118)
Add "__complete" to noDbCommands list so that Cobra's internal completion
command can run without requiring a beads database to be present.

Previously, running shell completions (e.g., `bd show <TAB>`) in a directory
without a .beads database would fail with "no beads database found" error.

Now completions return empty results gracefully when no database exists,
allowing basic command completion to work everywhere while issue ID
completion still requires a database.

Added integration test that verifies __complete command works without a database.
2026-01-15 19:22:55 -08:00

366 lines
9.9 KiB
Go

package main
import (
"context"
"os"
"testing"
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/storage/memory"
"github.com/steveyegge/beads/internal/types"
)
func TestIssueIDCompletion(t *testing.T) {
// Save original store and restore after test
originalStore := store
originalRootCtx := rootCtx
defer func() {
store = originalStore
rootCtx = originalRootCtx
}()
// Set up test context
ctx := context.Background()
rootCtx = ctx
// Create in-memory store for testing
memStore := memory.New("")
store = memStore
// Create test issues
testIssues := []*types.Issue{
{
ID: "bd-abc1",
Title: "Test issue 1",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
},
{
ID: "bd-abc2",
Title: "Test issue 2",
Status: types.StatusInProgress,
Priority: 2,
IssueType: types.TypeBug,
},
{
ID: "bd-xyz1",
Title: "Another test issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeFeature,
},
{
ID: "bd-xyz2",
Title: "Yet another test",
Status: types.StatusClosed,
Priority: 3,
IssueType: types.TypeTask,
ClosedAt: &[]time.Time{time.Now()}[0],
},
}
for _, issue := range testIssues {
if err := memStore.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create test issue: %v", err)
}
}
tests := []struct {
name string
toComplete string
expectedCount int
shouldContain []string
shouldNotContain []string
}{
{
name: "Empty prefix returns all issues",
toComplete: "",
expectedCount: 4,
shouldContain: []string{"bd-abc1", "bd-abc2", "bd-xyz1", "bd-xyz2"},
},
{
name: "Prefix 'bd-a' returns matching issues",
toComplete: "bd-a",
expectedCount: 2,
shouldContain: []string{"bd-abc1", "bd-abc2"},
shouldNotContain: []string{"bd-xyz1", "bd-xyz2"},
},
{
name: "Prefix 'bd-abc' returns matching issues",
toComplete: "bd-abc",
expectedCount: 2,
shouldContain: []string{"bd-abc1", "bd-abc2"},
shouldNotContain: []string{"bd-xyz1", "bd-xyz2"},
},
{
name: "Prefix 'bd-abc1' returns exact match",
toComplete: "bd-abc1",
expectedCount: 1,
shouldContain: []string{"bd-abc1"},
shouldNotContain: []string{"bd-abc2", "bd-xyz1", "bd-xyz2"},
},
{
name: "Prefix 'bd-xyz' returns matching issues",
toComplete: "bd-xyz",
expectedCount: 2,
shouldContain: []string{"bd-xyz1", "bd-xyz2"},
shouldNotContain: []string{"bd-abc1", "bd-abc2"},
},
{
name: "Non-matching prefix returns empty",
toComplete: "bd-zzz",
expectedCount: 0,
shouldContain: []string{},
shouldNotContain: []string{"bd-abc1", "bd-abc2", "bd-xyz1", "bd-xyz2"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a dummy command (not actually used by the function)
cmd := &cobra.Command{}
args := []string{}
// Call the completion function
completions, directive := issueIDCompletion(cmd, args, tt.toComplete)
// Check directive
if directive != cobra.ShellCompDirectiveNoFileComp {
t.Errorf("Expected directive NoFileComp (4), got %d", directive)
}
// Check count
if len(completions) != tt.expectedCount {
t.Errorf("Expected %d completions, got %d", tt.expectedCount, len(completions))
}
// Check that expected IDs are present
for _, expectedID := range tt.shouldContain {
found := false
for _, completion := range completions {
// Completion format is "ID\tTitle"
if len(completion) > 0 && completion[:len(expectedID)] == expectedID {
found = true
break
}
}
if !found {
t.Errorf("Expected completion to contain '%s', but it was not found", expectedID)
}
}
// Check that unexpected IDs are NOT present
for _, unexpectedID := range tt.shouldNotContain {
for _, completion := range completions {
if len(completion) > 0 && completion[:len(unexpectedID)] == unexpectedID {
t.Errorf("Did not expect completion to contain '%s', but it was found", unexpectedID)
}
}
}
// Verify format: each completion should be "ID\tTitle"
for _, completion := range completions {
if len(completion) == 0 {
t.Errorf("Got empty completion string")
continue
}
// Check that it contains a tab character
foundTab := false
for _, c := range completion {
if c == '\t' {
foundTab = true
break
}
}
if !foundTab {
t.Errorf("Completion '%s' doesn't contain tab separator", completion)
}
}
})
}
}
func TestIssueIDCompletion_NoStore(t *testing.T) {
// Save original store and restore after test
originalStore := store
originalDBPath := dbPath
defer func() {
store = originalStore
dbPath = originalDBPath
}()
// Set store to nil and dbPath to non-existent path
store = nil
dbPath = "/nonexistent/path/to/database.db"
cmd := &cobra.Command{}
args := []string{}
completions, directive := issueIDCompletion(cmd, args, "")
// Should return empty completions when store is nil and dbPath is invalid
if len(completions) != 0 {
t.Errorf("Expected 0 completions when store is nil and dbPath is invalid, got %d", len(completions))
}
if directive != cobra.ShellCompDirectiveNoFileComp {
t.Errorf("Expected directive NoFileComp (4), got %d", directive)
}
}
func TestCompleteCommandWorksWithoutDatabase(t *testing.T) {
// This test verifies that shell completions work even without a beads database.
// The __complete command must be in noDbCommands list so that PersistentPreRun
// doesn't exit with "no beads database found" error.
//
// This test will FAIL on versions before the fix was applied, where __complete
// was not in the noDbCommands list.
// Create a temp directory with no .beads database
tmpDir := t.TempDir()
// Save original state
originalDBPath := dbPath
originalStore := store
defer func() {
dbPath = originalDBPath
store = originalStore
}()
// Reset state to simulate no database
store = nil
dbPath = ""
// Change to temp directory (no database present)
originalWd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get working directory: %v", err)
}
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to temp directory: %v", err)
}
defer func() { _ = os.Chdir(originalWd) }()
// Test that issueIDCompletion returns gracefully (empty list, no error)
// when there's no database, rather than panicking or hanging
cmd := &cobra.Command{}
completions, directive := issueIDCompletion(cmd, []string{}, "")
// Should return empty completions, not panic
if completions == nil {
// nil is acceptable, convert to empty slice for consistent handling
completions = []string{}
}
// Should return NoFileComp directive
if directive != cobra.ShellCompDirectiveNoFileComp {
t.Errorf("Expected directive NoFileComp (%d), got %d",
cobra.ShellCompDirectiveNoFileComp, directive)
}
// Completions should be empty (no database to query)
if len(completions) != 0 {
t.Errorf("Expected 0 completions without database, got %d", len(completions))
}
}
func TestCompleteCommandInNoDbCommandsList(t *testing.T) {
// This integration test verifies that running `bd __complete show ""`
// does NOT fail with "no beads database found" error.
//
// The __complete command is Cobra's internal command for shell completions.
// It must be in the noDbCommands list to skip database initialization.
//
// Before the fix: this test would fail because __complete wasn't in noDbCommands,
// causing PersistentPreRun to exit with "no beads database found".
// After the fix: this test passes because __complete is in noDbCommands.
// Create a temp directory with no .beads database
tmpDir := t.TempDir()
// Change to temp directory (no database present)
originalWd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get working directory: %v", err)
}
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to temp directory: %v", err)
}
defer func() { _ = os.Chdir(originalWd) }()
// Save and reset global state
originalDBPath := dbPath
originalStore := store
originalDaemonClient := daemonClient
defer func() {
dbPath = originalDBPath
store = originalStore
daemonClient = originalDaemonClient
}()
store = nil
dbPath = ""
daemonClient = nil
// Capture stdout/stderr
oldStdout := os.Stdout
oldStderr := os.Stderr
_, w, _ := os.Pipe()
os.Stdout = w
os.Stderr = w
defer func() {
os.Stdout = oldStdout
os.Stderr = oldStderr
}()
// Run __complete command (this is what shell completion scripts call)
rootCmd.SetArgs([]string{"__complete", "show", ""})
err = rootCmd.Execute()
// Close pipe to get output
_ = w.Close()
// The command should NOT fail - if __complete is in noDbCommands,
// PersistentPreRun will skip database initialization and the completion
// will return empty results gracefully
if err != nil {
t.Errorf("__complete command failed without database: %v\n"+
"This indicates __complete is not in noDbCommands list.\n"+
"Shell completions should work without requiring a database.", err)
}
}
func TestIssueIDCompletion_EmptyDatabase(t *testing.T) {
// Save original store and restore after test
originalStore := store
originalRootCtx := rootCtx
defer func() {
store = originalStore
rootCtx = originalRootCtx
}()
// Set up test context
ctx := context.Background()
rootCtx = ctx
// Create empty in-memory store
memStore := memory.New("")
store = memStore
cmd := &cobra.Command{}
args := []string{}
completions, directive := issueIDCompletion(cmd, args, "")
// Should return empty completions when database is empty
if len(completions) != 0 {
t.Errorf("Expected 0 completions when database is empty, got %d", len(completions))
}
if directive != cobra.ShellCompDirectiveNoFileComp {
t.Errorf("Expected directive NoFileComp (4), got %d", directive)
}
}