Add intelligent shell completions that query the database to provide issue ID suggestions with titles for commands that take IDs as arguments. Changes: - Add issueIDCompletion function that queries storage for all issues - Register completion for show, update, close, edit, defer, undefer, reopen - Add comprehensive test suite with 3 test cases - Completions display ID with title as description (ID\tTitle format) The completion function opens the database (read-only) and filters issues based on the partially typed prefix, providing a better UX for commands that require issue IDs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
243 lines
6.2 KiB
Go
243 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"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 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)
|
|
}
|
|
}
|