Files
beads/cmd/bd/ready_test.go
Steve Yegge 53342732b5 feat: add --unassigned flag to bd ready command
Adds the ability to filter ready work for issues with no assignee,
which is useful for the SCAVENGE protocol in Gas Town where polecats
need to query the "Salvage Yard" for unclaimed work.

Changes:
- Add Unassigned bool field to types.WorkFilter
- Add --unassigned/-u flag to bd ready command
- Update SQL query in GetReadyWork to filter for NULL/empty assignee
- Add Unassigned field to RPC ReadyArgs for daemon support
- Add tests for the new functionality

Closes: gt-3rp

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 00:37:12 -08:00

263 lines
6.1 KiB
Go

package main
import (
"context"
"path/filepath"
"testing"
"time"
"github.com/steveyegge/beads/internal/types"
)
func TestReadySuite(t *testing.T) {
tmpDir := t.TempDir()
testDB := filepath.Join(tmpDir, ".beads", "beads.db")
s := newTestStore(t, testDB)
ctx := context.Background()
t.Run("ReadyWork", func(t *testing.T) {
// Create issues with different states
issues := []*types.Issue{
{
ID: "test-1",
Title: "Ready task 1",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
CreatedAt: time.Now(),
},
{
ID: "test-2",
Title: "Ready task 2",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
CreatedAt: time.Now(),
},
{
ID: "test-3",
Title: "Blocked task",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
CreatedAt: time.Now(),
},
{
ID: "test-blocker",
Title: "Blocking task",
Status: types.StatusOpen,
Priority: 0,
IssueType: types.TypeTask,
CreatedAt: time.Now(),
},
{
ID: "test-closed",
Title: "Closed task",
Status: types.StatusClosed,
Priority: 2,
IssueType: types.TypeTask,
CreatedAt: time.Now(),
ClosedAt: ptrTime(time.Now()),
},
}
for _, issue := range issues {
if err := s.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatal(err)
}
}
// Add dependency: test-3 depends on test-blocker
dep := &types.Dependency{
IssueID: "test-3",
DependsOnID: "test-blocker",
Type: types.DepBlocks,
CreatedAt: time.Now(),
}
if err := s.AddDependency(ctx, dep, "test"); err != nil {
t.Fatal(err)
}
// Test basic ready work
ready, err := s.GetReadyWork(ctx, types.WorkFilter{})
if err != nil {
t.Fatalf("GetReadyWork failed: %v", err)
}
// Should have test-1, test-2, test-blocker (not test-3 because it's blocked, not test-closed because it's closed)
if len(ready) < 3 {
t.Errorf("Expected at least 3 ready issues, got %d", len(ready))
}
// Check that test-3 is NOT in ready work
for _, issue := range ready {
if issue.ID == "test-3" {
t.Error("test-3 should not be in ready work (it's blocked)")
}
if issue.ID == "test-closed" {
t.Error("test-closed should not be in ready work (it's closed)")
}
}
// Test with priority filter
priority1 := 1
readyP1, err := s.GetReadyWork(ctx, types.WorkFilter{
Priority: &priority1,
})
if err != nil {
t.Fatalf("GetReadyWork with priority filter failed: %v", err)
}
// Should only have priority 1 issues
for _, issue := range readyP1 {
if issue.Priority != 1 {
t.Errorf("Expected priority 1, got %d for issue %s", issue.Priority, issue.ID)
}
}
// Test with limit
readyLimited, err := s.GetReadyWork(ctx, types.WorkFilter{
Limit: 1,
})
if err != nil {
t.Fatalf("GetReadyWork with limit failed: %v", err)
}
if len(readyLimited) > 1 {
t.Errorf("Expected at most 1 issue with limit=1, got %d", len(readyLimited))
}
})
t.Run("ReadyWorkWithAssignee", func(t *testing.T) {
// Create issues with different assignees
issues := []*types.Issue{
{
ID: "test-alice",
Title: "Alice's task",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
Assignee: "alice",
CreatedAt: time.Now(),
},
{
ID: "test-bob",
Title: "Bob's task",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
Assignee: "bob",
CreatedAt: time.Now(),
},
{
ID: "test-unassigned",
Title: "Unassigned task",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
CreatedAt: time.Now(),
},
}
for _, issue := range issues {
if err := s.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatal(err)
}
}
// Test filtering by assignee
alice := "alice"
readyAlice, err := s.GetReadyWork(ctx, types.WorkFilter{
Assignee: &alice,
})
if err != nil {
t.Fatalf("GetReadyWork with assignee filter failed: %v", err)
}
if len(readyAlice) != 1 {
t.Errorf("Expected 1 issue for alice, got %d", len(readyAlice))
}
if len(readyAlice) > 0 && readyAlice[0].Assignee != "alice" {
t.Errorf("Expected assignee='alice', got %q", readyAlice[0].Assignee)
}
})
t.Run("ReadyWorkUnassigned", func(t *testing.T) {
// Test filtering for unassigned issues
readyUnassigned, err := s.GetReadyWork(ctx, types.WorkFilter{
Unassigned: true,
})
if err != nil {
t.Fatalf("GetReadyWork with unassigned filter failed: %v", err)
}
// All returned issues should have no assignee
for _, issue := range readyUnassigned {
if issue.Assignee != "" {
t.Errorf("Expected empty assignee, got %q for issue %s", issue.Assignee, issue.ID)
}
}
// Should include test-unassigned from previous test
found := false
for _, issue := range readyUnassigned {
if issue.ID == "test-unassigned" {
found = true
break
}
}
if !found {
t.Error("Expected to find test-unassigned in unassigned results")
}
})
t.Run("ReadyWorkInProgress", func(t *testing.T) {
// Create in-progress issue (should be in ready work)
issue := &types.Issue{
ID: "test-wip",
Title: "Work in progress",
Status: types.StatusInProgress,
Priority: 1,
IssueType: types.TypeTask,
CreatedAt: time.Now(),
}
if err := s.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatal(err)
}
// Test that in-progress shows up in ready work
ready, err := s.GetReadyWork(ctx, types.WorkFilter{})
if err != nil {
t.Fatalf("GetReadyWork failed: %v", err)
}
found := false
for _, i := range ready {
if i.ID == "test-wip" {
found = true
break
}
}
if !found {
t.Error("In-progress issue should appear in ready work")
}
})
}
func TestReadyCommandInit(t *testing.T) {
if readyCmd == nil {
t.Fatal("readyCmd should be initialized")
}
if readyCmd.Use != "ready" {
t.Errorf("Expected Use='ready', got %q", readyCmd.Use)
}
if len(readyCmd.Short) == 0 {
t.Error("readyCmd should have Short description")
}
}