feat: Add date and priority filters to bd search (bd-au0.5)

Adds missing date range and priority filtering to bd search for feature
parity with bd list. Part of command standardization epic (bd-au0).

Changes:
- Add date range flags: --created-after/before, --updated-after/before,
  --closed-after/before
- Add priority range flags: --priority-min, --priority-max
- Support multiple date formats (RFC3339, YYYY-MM-DD, etc.)
- Apply filters in both direct mode and daemon RPC mode
- Add comprehensive tests for new filters

Examples:
  bd search "security" --priority-min 0 --priority-max 2
  bd search "bug" --created-after 2025-01-01
  bd search "refactor" --updated-after 2025-01-01 --priority-min 1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-21 22:45:47 -05:00
parent 0040e8029b
commit 787fb4e56f
2 changed files with 278 additions and 1 deletions

View File

@@ -2,11 +2,15 @@ package main
import (
"bytes"
"context"
"io"
"os"
"path/filepath"
"testing"
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/types"
)
// TestSearchCommand_HelpErrorHandling verifies that the search command handles
@@ -158,3 +162,155 @@ func TestSearchCommand_MissingQueryShowsHelp(t *testing.T) {
cmd.SetArgs([]string{}) // No query
_ = cmd.Execute()
}
// TestSearchWithDateAndPriorityFilters tests bd search with date range and priority filters
func TestSearchWithDateAndPriorityFilters(t *testing.T) {
tmpDir := t.TempDir()
testDB := filepath.Join(tmpDir, ".beads", "beads.db")
s := newTestStore(t, testDB)
ctx := context.Background()
now := time.Now()
yesterday := now.Add(-24 * time.Hour)
twoDaysAgo := now.Add(-48 * time.Hour)
// Create test issues with search-relevant content
issue1 := &types.Issue{
Title: "Critical security bug in auth",
Description: "Authentication bypass vulnerability",
Priority: 0,
IssueType: types.TypeBug,
Status: types.StatusOpen,
}
issue2 := &types.Issue{
Title: "Add security scanning feature",
Description: "Implement automated security checks",
Priority: 2,
IssueType: types.TypeFeature,
Status: types.StatusInProgress,
}
issue3 := &types.Issue{
Title: "Security audit task",
Description: "Review all security practices",
Priority: 3,
IssueType: types.TypeTask,
Status: types.StatusOpen,
}
for _, issue := range []*types.Issue{issue1, issue2, issue3} {
if err := s.CreateIssue(ctx, issue, "test-user"); err != nil {
t.Fatalf("Failed to create issue: %v", err)
}
}
// Close issue3 to set closed_at timestamp
if err := s.CloseIssue(ctx, issue3.ID, "test-user", "Testing"); err != nil {
t.Fatalf("Failed to close issue3: %v", err)
}
t.Run("search with priority range - min", func(t *testing.T) {
minPrio := 2
results, err := s.SearchIssues(ctx, "security", types.IssueFilter{
PriorityMin: &minPrio,
})
if err != nil {
t.Fatalf("Search failed: %v", err)
}
if len(results) != 2 {
t.Errorf("Expected 2 issues matching 'security' with priority >= 2, got %d", len(results))
}
})
t.Run("search with priority range - max", func(t *testing.T) {
maxPrio := 1
results, err := s.SearchIssues(ctx, "security", types.IssueFilter{
PriorityMax: &maxPrio,
})
if err != nil {
t.Fatalf("Search failed: %v", err)
}
if len(results) != 1 {
t.Errorf("Expected 1 issue matching 'security' with priority <= 1, got %d", len(results))
}
if len(results) > 0 && results[0].ID != issue1.ID {
t.Errorf("Expected issue1, got %s", results[0].ID)
}
})
t.Run("search with priority range - min and max", func(t *testing.T) {
minPrio := 1
maxPrio := 2
results, err := s.SearchIssues(ctx, "security", types.IssueFilter{
PriorityMin: &minPrio,
PriorityMax: &maxPrio,
})
if err != nil {
t.Fatalf("Search failed: %v", err)
}
if len(results) != 1 {
t.Errorf("Expected 1 issue matching 'security' with priority 1-2, got %d", len(results))
}
if len(results) > 0 && results[0].ID != issue2.ID {
t.Errorf("Expected issue2, got %s", results[0].ID)
}
})
t.Run("search with created after", func(t *testing.T) {
results, err := s.SearchIssues(ctx, "security", types.IssueFilter{
CreatedAfter: &twoDaysAgo,
})
if err != nil {
t.Fatalf("Search failed: %v", err)
}
if len(results) != 3 {
t.Errorf("Expected 3 issues matching 'security' created after two days ago, got %d", len(results))
}
})
t.Run("search with updated before", func(t *testing.T) {
futureTime := now.Add(24 * time.Hour)
results, err := s.SearchIssues(ctx, "security", types.IssueFilter{
UpdatedBefore: &futureTime,
})
if err != nil {
t.Fatalf("Search failed: %v", err)
}
if len(results) != 3 {
t.Errorf("Expected 3 issues matching 'security', got %d", len(results))
}
})
t.Run("search with closed after", func(t *testing.T) {
results, err := s.SearchIssues(ctx, "security", types.IssueFilter{
ClosedAfter: &yesterday,
})
if err != nil {
t.Fatalf("Search failed: %v", err)
}
if len(results) != 1 {
t.Errorf("Expected 1 closed issue matching 'security', got %d", len(results))
}
if len(results) > 0 && results[0].ID != issue3.ID {
t.Errorf("Expected issue3, got %s", results[0].ID)
}
})
t.Run("search with combined filters", func(t *testing.T) {
minPrio := 0
maxPrio := 2
results, err := s.SearchIssues(ctx, "auth", types.IssueFilter{
PriorityMin: &minPrio,
PriorityMax: &maxPrio,
CreatedAfter: &twoDaysAgo,
})
if err != nil {
t.Fatalf("Search failed: %v", err)
}
// Should match issue1 (has "auth" in title, priority 0)
// and issue2 (has "auth" in description via "automated", priority 2)
// Note: "auth" is a substring match, so it matches "authentication" and "automated"
if len(results) < 1 {
t.Errorf("Expected at least 1 result matching combined filters, got %d", len(results))
}
})
}