fix: code review fixes for Transaction API and Search (epic bd-8bq)
- Add safe type assertions in applyUpdatesToIssue (bd-4gs) - Add --sort and --reverse flags to bd search (bd-4f6) - Add test cases for SearchIssues priority range, date range, IDs (bd-ew5) - Handle errors from GetLabelsForIssues in search.go (bd-lce) - Standardize error wrapping to fmt.Errorf pattern (bd-7kl) - Extract shared scanIssueRow helper function (bd-ajf) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,9 @@ Examples:
|
||||
bd search "bd-5q" # Search by partial ID
|
||||
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`,
|
||||
bd search "refactor" --updated-after 2025-01-01 --priority-min 1
|
||||
bd search "bug" --sort priority
|
||||
bd search "task" --sort created --reverse`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Get query from args or --query flag
|
||||
queryFlag, _ := cmd.Flags().GetString("query")
|
||||
@@ -70,6 +72,10 @@ Examples:
|
||||
priorityMinStr, _ := cmd.Flags().GetString("priority-min")
|
||||
priorityMaxStr, _ := cmd.Flags().GetString("priority-max")
|
||||
|
||||
// Sort flags
|
||||
sortBy, _ := cmd.Flags().GetString("sort")
|
||||
reverse, _ := cmd.Flags().GetBool("reverse")
|
||||
|
||||
// Normalize labels
|
||||
labels = util.NormalizeLabels(labels)
|
||||
labelsAny = util.NormalizeLabels(labelsAny)
|
||||
@@ -279,8 +285,16 @@ Examples:
|
||||
for i, issue := range issues {
|
||||
issueIDs[i] = issue.ID
|
||||
}
|
||||
labelsMap, _ := store.GetLabelsForIssues(ctx, issueIDs)
|
||||
depCounts, _ := store.GetDependencyCounts(ctx, issueIDs)
|
||||
labelsMap, err := store.GetLabelsForIssues(ctx, issueIDs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to get labels: %v\n", err)
|
||||
labelsMap = make(map[string][]string)
|
||||
}
|
||||
depCounts, err := store.GetDependencyCounts(ctx, issueIDs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to get dependency counts: %v\n", err)
|
||||
depCounts = make(map[string]*types.DependencyCounts)
|
||||
}
|
||||
|
||||
// Populate labels
|
||||
for _, issue := range issues {
|
||||
@@ -382,5 +396,9 @@ func init() {
|
||||
searchCmd.Flags().String("priority-min", "", "Filter by minimum priority (inclusive, 0-4 or P0-P4)")
|
||||
searchCmd.Flags().String("priority-max", "", "Filter by maximum priority (inclusive, 0-4 or P0-P4)")
|
||||
|
||||
// Sort flags
|
||||
searchCmd.Flags().String("sort", "", "Sort by field: priority, created, updated, closed, status, id, title, type, assignee")
|
||||
searchCmd.Flags().BoolP("reverse", "r", false, "Reverse sort order")
|
||||
|
||||
rootCmd.AddCommand(searchCmd)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -120,13 +121,13 @@ func (t *sqliteTxStorage) CreateIssue(ctx context.Context, issue *types.Issue, a
|
||||
// Generate hash-based ID with adaptive length based on database size (bd-ea2a13)
|
||||
generatedID, err := GenerateIssueID(ctx, t.conn, prefix, issue, actor)
|
||||
if err != nil {
|
||||
return wrapDBError("generate issue ID", err)
|
||||
return fmt.Errorf("failed to generate issue ID: %w", err)
|
||||
}
|
||||
issue.ID = generatedID
|
||||
} else {
|
||||
// Validate that explicitly provided ID matches the configured prefix (bd-177)
|
||||
if err := ValidateIssueIDPrefix(issue.ID, prefix); err != nil {
|
||||
return wrapDBError("validate issue ID prefix", err)
|
||||
return fmt.Errorf("failed to validate issue ID prefix: %w", err)
|
||||
}
|
||||
|
||||
// For hierarchical IDs (bd-a3f8e9.1), ensure parent exists
|
||||
@@ -147,17 +148,17 @@ func (t *sqliteTxStorage) CreateIssue(ctx context.Context, issue *types.Issue, a
|
||||
|
||||
// Insert issue
|
||||
if err := insertIssue(ctx, t.conn, issue); err != nil {
|
||||
return wrapDBError("insert issue", err)
|
||||
return fmt.Errorf("failed to insert issue: %w", err)
|
||||
}
|
||||
|
||||
// Record creation event
|
||||
if err := recordCreatedEvent(ctx, t.conn, issue, actor); err != nil {
|
||||
return wrapDBError("record creation event", err)
|
||||
return fmt.Errorf("failed to record creation event: %w", err)
|
||||
}
|
||||
|
||||
// Mark issue as dirty for incremental export
|
||||
if err := markDirty(ctx, t.conn, issue.ID); err != nil {
|
||||
return wrapDBError("mark issue dirty", err)
|
||||
return fmt.Errorf("failed to mark issue dirty: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -196,12 +197,12 @@ func (t *sqliteTxStorage) CreateIssues(ctx context.Context, issues []*types.Issu
|
||||
if issue.ID == "" {
|
||||
generatedID, err := GenerateIssueID(ctx, t.conn, prefix, issue, actor)
|
||||
if err != nil {
|
||||
return wrapDBError("generate issue ID", err)
|
||||
return fmt.Errorf("failed to generate issue ID: %w", err)
|
||||
}
|
||||
issue.ID = generatedID
|
||||
} else {
|
||||
if err := ValidateIssueIDPrefix(issue.ID, prefix); err != nil {
|
||||
return wrapDBError("validate issue ID prefix", err)
|
||||
return fmt.Errorf("failed to validate issue ID prefix: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,17 +218,17 @@ func (t *sqliteTxStorage) CreateIssues(ctx context.Context, issues []*types.Issu
|
||||
|
||||
// Insert all issues
|
||||
if err := insertIssues(ctx, t.conn, issues); err != nil {
|
||||
return wrapDBError("insert issues", err)
|
||||
return fmt.Errorf("failed to insert issues: %w", err)
|
||||
}
|
||||
|
||||
// Record creation events
|
||||
if err := recordCreatedEvents(ctx, t.conn, issues, actor); err != nil {
|
||||
return wrapDBError("record creation events", err)
|
||||
return fmt.Errorf("failed to record creation events: %w", err)
|
||||
}
|
||||
|
||||
// Mark all issues as dirty
|
||||
if err := markDirtyBatch(ctx, t.conn, issues); err != nil {
|
||||
return wrapDBError("mark issues dirty", err)
|
||||
return fmt.Errorf("failed to mark issues dirty: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -236,68 +237,23 @@ func (t *sqliteTxStorage) CreateIssues(ctx context.Context, issues []*types.Issu
|
||||
// GetIssue retrieves an issue within the transaction.
|
||||
// This enables read-your-writes semantics within the transaction.
|
||||
func (t *sqliteTxStorage) GetIssue(ctx context.Context, id string) (*types.Issue, error) {
|
||||
var issue types.Issue
|
||||
var closedAt sql.NullTime
|
||||
var estimatedMinutes sql.NullInt64
|
||||
var assignee sql.NullString
|
||||
var externalRef sql.NullString
|
||||
var compactedAt sql.NullTime
|
||||
var originalSize sql.NullInt64
|
||||
var sourceRepo sql.NullString
|
||||
var contentHash sql.NullString
|
||||
var compactedAtCommit sql.NullString
|
||||
|
||||
err := t.conn.QueryRowContext(ctx, `
|
||||
row := t.conn.QueryRowContext(ctx, `
|
||||
SELECT id, content_hash, title, description, design, acceptance_criteria, notes,
|
||||
status, priority, issue_type, assignee, estimated_minutes,
|
||||
created_at, updated_at, closed_at, external_ref,
|
||||
compaction_level, compacted_at, compacted_at_commit, original_size, source_repo
|
||||
FROM issues
|
||||
WHERE id = ?
|
||||
`, id).Scan(
|
||||
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
||||
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
||||
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef,
|
||||
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo,
|
||||
)
|
||||
`, id)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
issue, err := scanIssueRow(row)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get issue: %w", err)
|
||||
}
|
||||
|
||||
if contentHash.Valid {
|
||||
issue.ContentHash = contentHash.String
|
||||
}
|
||||
if closedAt.Valid {
|
||||
issue.ClosedAt = &closedAt.Time
|
||||
}
|
||||
if estimatedMinutes.Valid {
|
||||
mins := int(estimatedMinutes.Int64)
|
||||
issue.EstimatedMinutes = &mins
|
||||
}
|
||||
if assignee.Valid {
|
||||
issue.Assignee = assignee.String
|
||||
}
|
||||
if externalRef.Valid {
|
||||
issue.ExternalRef = &externalRef.String
|
||||
}
|
||||
if compactedAt.Valid {
|
||||
issue.CompactedAt = &compactedAt.Time
|
||||
}
|
||||
if compactedAtCommit.Valid {
|
||||
issue.CompactedAtCommit = &compactedAtCommit.String
|
||||
}
|
||||
if originalSize.Valid {
|
||||
issue.OriginalSize = int(originalSize.Int64)
|
||||
}
|
||||
if sourceRepo.Valid {
|
||||
issue.SourceRepo = sourceRepo.String
|
||||
}
|
||||
|
||||
// Fetch labels for this issue using the transaction connection
|
||||
labels, err := t.getLabels(ctx, issue.ID)
|
||||
if err != nil {
|
||||
@@ -305,7 +261,7 @@ func (t *sqliteTxStorage) GetIssue(ctx context.Context, id string) (*types.Issue
|
||||
}
|
||||
issue.Labels = labels
|
||||
|
||||
return &issue, nil
|
||||
return issue, nil
|
||||
}
|
||||
|
||||
// getLabels retrieves labels using the transaction's connection
|
||||
@@ -335,7 +291,7 @@ func (t *sqliteTxStorage) UpdateIssue(ctx context.Context, id string, updates ma
|
||||
// Get old issue for event
|
||||
oldIssue, err := t.GetIssue(ctx, id)
|
||||
if err != nil {
|
||||
return wrapDBError("get issue for update", err)
|
||||
return fmt.Errorf("failed to get issue for update: %w", err)
|
||||
}
|
||||
if oldIssue == nil {
|
||||
return fmt.Errorf("issue %s not found", id)
|
||||
@@ -353,7 +309,7 @@ func (t *sqliteTxStorage) UpdateIssue(ctx context.Context, id string, updates ma
|
||||
|
||||
// Validate field values
|
||||
if err := validateFieldUpdate(key, value); err != nil {
|
||||
return wrapDBError("validate field update", err)
|
||||
return fmt.Errorf("failed to validate field update: %w", err)
|
||||
}
|
||||
|
||||
setClauses = append(setClauses, fmt.Sprintf("%s = ?", key))
|
||||
@@ -430,34 +386,46 @@ func applyUpdatesToIssue(issue *types.Issue, updates map[string]interface{}) {
|
||||
for key, value := range updates {
|
||||
switch key {
|
||||
case "title":
|
||||
issue.Title = value.(string)
|
||||
if s, ok := value.(string); ok {
|
||||
issue.Title = s
|
||||
}
|
||||
case "description":
|
||||
issue.Description = value.(string)
|
||||
if s, ok := value.(string); ok {
|
||||
issue.Description = s
|
||||
}
|
||||
case "design":
|
||||
issue.Design = value.(string)
|
||||
if s, ok := value.(string); ok {
|
||||
issue.Design = s
|
||||
}
|
||||
case "acceptance_criteria":
|
||||
issue.AcceptanceCriteria = value.(string)
|
||||
if s, ok := value.(string); ok {
|
||||
issue.AcceptanceCriteria = s
|
||||
}
|
||||
case "notes":
|
||||
issue.Notes = value.(string)
|
||||
if s, ok := value.(string); ok {
|
||||
issue.Notes = s
|
||||
}
|
||||
case "status":
|
||||
if s, ok := value.(types.Status); ok {
|
||||
issue.Status = s
|
||||
} else {
|
||||
issue.Status = types.Status(value.(string))
|
||||
} else if s, ok := value.(string); ok {
|
||||
issue.Status = types.Status(s)
|
||||
}
|
||||
case "priority":
|
||||
issue.Priority = value.(int)
|
||||
if p, ok := value.(int); ok {
|
||||
issue.Priority = p
|
||||
}
|
||||
case "issue_type":
|
||||
if t, ok := value.(types.IssueType); ok {
|
||||
issue.IssueType = t
|
||||
} else {
|
||||
issue.IssueType = types.IssueType(value.(string))
|
||||
} else if s, ok := value.(string); ok {
|
||||
issue.IssueType = types.IssueType(s)
|
||||
}
|
||||
case "assignee":
|
||||
if value == nil {
|
||||
issue.Assignee = ""
|
||||
} else {
|
||||
issue.Assignee = value.(string)
|
||||
} else if s, ok := value.(string); ok {
|
||||
issue.Assignee = s
|
||||
}
|
||||
case "external_ref":
|
||||
if value == nil {
|
||||
@@ -1027,7 +995,8 @@ func (t *sqliteTxStorage) SearchIssues(ctx context.Context, query string, filter
|
||||
querySQL := fmt.Sprintf(`
|
||||
SELECT id, content_hash, title, description, design, acceptance_criteria, notes,
|
||||
status, priority, issue_type, assignee, estimated_minutes,
|
||||
created_at, updated_at, closed_at, external_ref, source_repo
|
||||
created_at, updated_at, closed_at, external_ref,
|
||||
compaction_level, compacted_at, compacted_at_commit, original_size, source_repo
|
||||
FROM issues
|
||||
%s
|
||||
ORDER BY priority ASC, created_at DESC
|
||||
@@ -1043,26 +1012,32 @@ func (t *sqliteTxStorage) SearchIssues(ctx context.Context, query string, filter
|
||||
return t.scanIssues(ctx, rows)
|
||||
}
|
||||
|
||||
// scanIssues scans issue rows and fetches labels using the transaction connection.
|
||||
func (t *sqliteTxStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*types.Issue, error) {
|
||||
var issues []*types.Issue
|
||||
var issueIDs []string
|
||||
// scanner is an interface that both *sql.Row and *sql.Rows satisfy
|
||||
type scanner interface {
|
||||
Scan(dest ...interface{}) error
|
||||
}
|
||||
|
||||
// First pass: scan all issues
|
||||
for rows.Next() {
|
||||
// scanIssueRow scans a single issue row from the database.
|
||||
// This is a shared helper used by both GetIssue and SearchIssues to ensure
|
||||
// consistent scanning of issue rows.
|
||||
func scanIssueRow(row scanner) (*types.Issue, error) {
|
||||
var issue types.Issue
|
||||
var contentHash sql.NullString
|
||||
var closedAt sql.NullTime
|
||||
var estimatedMinutes sql.NullInt64
|
||||
var assignee sql.NullString
|
||||
var externalRef sql.NullString
|
||||
var compactedAt sql.NullTime
|
||||
var originalSize sql.NullInt64
|
||||
var sourceRepo sql.NullString
|
||||
var compactedAtCommit sql.NullString
|
||||
|
||||
err := rows.Scan(
|
||||
err := row.Scan(
|
||||
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
||||
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
||||
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo,
|
||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef,
|
||||
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan issue: %w", err)
|
||||
@@ -1084,11 +1059,34 @@ func (t *sqliteTxStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*ty
|
||||
if externalRef.Valid {
|
||||
issue.ExternalRef = &externalRef.String
|
||||
}
|
||||
if compactedAt.Valid {
|
||||
issue.CompactedAt = &compactedAt.Time
|
||||
}
|
||||
if compactedAtCommit.Valid {
|
||||
issue.CompactedAtCommit = &compactedAtCommit.String
|
||||
}
|
||||
if originalSize.Valid {
|
||||
issue.OriginalSize = int(originalSize.Int64)
|
||||
}
|
||||
if sourceRepo.Valid {
|
||||
issue.SourceRepo = sourceRepo.String
|
||||
}
|
||||
|
||||
issues = append(issues, &issue)
|
||||
return &issue, nil
|
||||
}
|
||||
|
||||
// scanIssues scans issue rows and fetches labels using the transaction connection.
|
||||
func (t *sqliteTxStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*types.Issue, error) {
|
||||
var issues []*types.Issue
|
||||
var issueIDs []string
|
||||
|
||||
// First pass: scan all issues
|
||||
for rows.Next() {
|
||||
issue, err := scanIssueRow(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issues = append(issues, issue)
|
||||
issueIDs = append(issueIDs, issue.ID)
|
||||
}
|
||||
|
||||
|
||||
@@ -1663,3 +1663,192 @@ func TestTransactionSearchIssuesLimit(t *testing.T) {
|
||||
t.Fatalf("RunInTransaction failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTransactionSearchIssuesWithPriorityRange tests priority range filters within transaction.
|
||||
func TestTransactionSearchIssuesWithPriorityRange(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
|
||||
// Create issues with different priorities
|
||||
for i := 0; i < 5; i++ {
|
||||
issue := &types.Issue{
|
||||
Title: "Priority Range Test",
|
||||
Status: types.StatusOpen,
|
||||
Priority: i, // P0, P1, P2, P3, P4
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
if err := tx.CreateIssue(ctx, issue, "test-actor"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by PriorityMin only (P2 and higher priority = lower number)
|
||||
minPriority := 2
|
||||
results, err := tx.SearchIssues(ctx, "", types.IssueFilter{PriorityMin: &minPriority})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Should get P2, P3, P4
|
||||
if len(results) != 3 {
|
||||
t.Errorf("expected 3 issues with priority >= 2, got %d", len(results))
|
||||
}
|
||||
|
||||
// Filter by PriorityMax only
|
||||
maxPriority := 1
|
||||
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{PriorityMax: &maxPriority})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Should get P0, P1
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 issues with priority <= 1, got %d", len(results))
|
||||
}
|
||||
|
||||
// Filter by priority range
|
||||
minP := 1
|
||||
maxP := 3
|
||||
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{PriorityMin: &minP, PriorityMax: &maxP})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Should get P1, P2, P3
|
||||
if len(results) != 3 {
|
||||
t.Errorf("expected 3 issues with priority 1-3, got %d", len(results))
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("RunInTransaction failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTransactionSearchIssuesWithDateRange tests date range filters within transaction.
|
||||
func TestTransactionSearchIssuesWithDateRange(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
|
||||
now := time.Now()
|
||||
past := now.Add(-48 * time.Hour)
|
||||
future := now.Add(24 * time.Hour)
|
||||
|
||||
// Create issues - CreatedAt is set automatically
|
||||
issue1 := &types.Issue{
|
||||
Title: "Recent Issue",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
if err := tx.CreateIssue(ctx, issue1, "test-actor"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Filter by CreatedAfter (should find the issue created just now)
|
||||
createdAfter := past
|
||||
results, err := tx.SearchIssues(ctx, "", types.IssueFilter{CreatedAfter: &createdAfter})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(results) != 1 {
|
||||
t.Errorf("expected 1 issue created after past, got %d", len(results))
|
||||
}
|
||||
|
||||
// Filter by CreatedBefore with future time (should find the issue)
|
||||
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{CreatedBefore: &future})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(results) != 1 {
|
||||
t.Errorf("expected 1 issue created before future, got %d", len(results))
|
||||
}
|
||||
|
||||
// Filter by CreatedBefore with past time (should find nothing)
|
||||
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{CreatedBefore: &past})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(results) != 0 {
|
||||
t.Errorf("expected 0 issues created before past, got %d", len(results))
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("RunInTransaction failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTransactionSearchIssuesWithIDs tests IDs filter within transaction.
|
||||
func TestTransactionSearchIssuesWithIDs(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
|
||||
// Create several issues and collect their IDs
|
||||
var issueIDs []string
|
||||
for i := 0; i < 5; i++ {
|
||||
issue := &types.Issue{
|
||||
Title: "IDs Filter Test",
|
||||
Status: types.StatusOpen,
|
||||
Priority: i,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
if err := tx.CreateIssue(ctx, issue, "test-actor"); err != nil {
|
||||
return err
|
||||
}
|
||||
issueIDs = append(issueIDs, issue.ID)
|
||||
}
|
||||
|
||||
// Filter by specific IDs (first 2)
|
||||
results, err := tx.SearchIssues(ctx, "", types.IssueFilter{IDs: issueIDs[:2]})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 issues when filtering by 2 IDs, got %d", len(results))
|
||||
}
|
||||
|
||||
// Filter by single ID
|
||||
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{IDs: []string{issueIDs[0]}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(results) != 1 {
|
||||
t.Errorf("expected 1 issue when filtering by 1 ID, got %d", len(results))
|
||||
}
|
||||
if results[0].ID != issueIDs[0] {
|
||||
t.Errorf("expected issue ID %s, got %s", issueIDs[0], results[0].ID)
|
||||
}
|
||||
|
||||
// Filter by non-existent ID
|
||||
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{IDs: []string{"nonexistent-id"}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(results) != 0 {
|
||||
t.Errorf("expected 0 issues for non-existent ID, got %d", len(results))
|
||||
}
|
||||
|
||||
// Empty IDs filter should return all issues
|
||||
results, err = tx.SearchIssues(ctx, "", types.IssueFilter{IDs: []string{}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(results) != 5 {
|
||||
t.Errorf("expected 5 issues with empty IDs filter, got %d", len(results))
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("RunInTransaction failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user