package sqlite import ( "context" "testing" "github.com/steveyegge/beads/internal/types" ) func TestGetReadyWork(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create issues: // bd-1: open, no dependencies → READY // bd-2: open, depends on bd-1 (open) → BLOCKED // bd-3: open, no dependencies → READY // bd-4: closed, no dependencies → NOT READY (closed) // bd-5: open, depends on bd-4 (closed) → READY (blocker is closed) issue1 := &types.Issue{Title: "Ready 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} issue2 := &types.Issue{Title: "Blocked", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} issue3 := &types.Issue{Title: "Ready 2", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask} issue4 := &types.Issue{Title: "Closed", Status: types.StatusClosed, Priority: 1, IssueType: types.TypeTask} issue5 := &types.Issue{Title: "Ready 3", Status: types.StatusOpen, Priority: 0, IssueType: types.TypeTask} store.CreateIssue(ctx, issue1, "test-user") store.CreateIssue(ctx, issue2, "test-user") store.CreateIssue(ctx, issue3, "test-user") store.CreateIssue(ctx, issue4, "test-user") store.CloseIssue(ctx, issue4.ID, "Done", "test-user") store.CreateIssue(ctx, issue5, "test-user") // Add dependencies store.AddDependency(ctx, &types.Dependency{IssueID: issue2.ID, DependsOnID: issue1.ID, Type: types.DepBlocks}, "test-user") store.AddDependency(ctx, &types.Dependency{IssueID: issue5.ID, DependsOnID: issue4.ID, Type: types.DepBlocks}, "test-user") // Get ready work ready, err := store.GetReadyWork(ctx, types.WorkFilter{Status: types.StatusOpen}) if err != nil { t.Fatalf("GetReadyWork failed: %v", err) } // Should have 3 ready issues: bd-1, bd-3, bd-5 if len(ready) != 3 { t.Fatalf("Expected 3 ready issues, got %d", len(ready)) } // Verify ready issues readyIDs := make(map[string]bool) for _, issue := range ready { readyIDs[issue.ID] = true } if !readyIDs[issue1.ID] { t.Errorf("Expected %s to be ready", issue1.ID) } if !readyIDs[issue3.ID] { t.Errorf("Expected %s to be ready", issue3.ID) } if !readyIDs[issue5.ID] { t.Errorf("Expected %s to be ready", issue5.ID) } if readyIDs[issue2.ID] { t.Errorf("Expected %s to be blocked, but it was ready", issue2.ID) } } func TestGetReadyWorkPriorityOrder(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create issues with different priorities issueP0 := &types.Issue{Title: "Highest", Status: types.StatusOpen, Priority: 0, IssueType: types.TypeTask} issueP2 := &types.Issue{Title: "Medium", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask} issueP1 := &types.Issue{Title: "High", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} store.CreateIssue(ctx, issueP2, "test-user") store.CreateIssue(ctx, issueP0, "test-user") store.CreateIssue(ctx, issueP1, "test-user") // Get ready work ready, err := store.GetReadyWork(ctx, types.WorkFilter{Status: types.StatusOpen}) if err != nil { t.Fatalf("GetReadyWork failed: %v", err) } if len(ready) != 3 { t.Fatalf("Expected 3 ready issues, got %d", len(ready)) } // Verify priority ordering (P0 first, then P1, then P2) if ready[0].Priority != 0 { t.Errorf("Expected first issue to be P0, got P%d", ready[0].Priority) } if ready[1].Priority != 1 { t.Errorf("Expected second issue to be P1, got P%d", ready[1].Priority) } if ready[2].Priority != 2 { t.Errorf("Expected third issue to be P2, got P%d", ready[2].Priority) } } func TestGetReadyWorkWithPriorityFilter(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create issues with different priorities issueP0 := &types.Issue{Title: "P0", Status: types.StatusOpen, Priority: 0, IssueType: types.TypeTask} issueP1 := &types.Issue{Title: "P1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} issueP2 := &types.Issue{Title: "P2", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask} store.CreateIssue(ctx, issueP0, "test-user") store.CreateIssue(ctx, issueP1, "test-user") store.CreateIssue(ctx, issueP2, "test-user") // Filter for P0 only priority0 := 0 ready, err := store.GetReadyWork(ctx, types.WorkFilter{Status: types.StatusOpen, Priority: &priority0}) if err != nil { t.Fatalf("GetReadyWork failed: %v", err) } if len(ready) != 1 { t.Fatalf("Expected 1 P0 issue, got %d", len(ready)) } if ready[0].Priority != 0 { t.Errorf("Expected P0 issue, got P%d", ready[0].Priority) } } func TestGetReadyWorkWithAssigneeFilter(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create issues with different assignees issueAlice := &types.Issue{Title: "Alice's task", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask, Assignee: "alice"} issueBob := &types.Issue{Title: "Bob's task", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask, Assignee: "bob"} issueUnassigned := &types.Issue{Title: "Unassigned", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} store.CreateIssue(ctx, issueAlice, "test-user") store.CreateIssue(ctx, issueBob, "test-user") store.CreateIssue(ctx, issueUnassigned, "test-user") // Filter for alice assignee := "alice" ready, err := store.GetReadyWork(ctx, types.WorkFilter{Status: types.StatusOpen, Assignee: &assignee}) if err != nil { t.Fatalf("GetReadyWork failed: %v", err) } if len(ready) != 1 { t.Fatalf("Expected 1 issue for alice, got %d", len(ready)) } if ready[0].Assignee != "alice" { t.Errorf("Expected alice's issue, got %s", ready[0].Assignee) } } func TestGetReadyWorkWithLimit(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create 5 ready issues for i := 0; i < 5; i++ { issue := &types.Issue{Title: "Task", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask} store.CreateIssue(ctx, issue, "test-user") } // Limit to 3 ready, err := store.GetReadyWork(ctx, types.WorkFilter{Status: types.StatusOpen, Limit: 3}) if err != nil { t.Fatalf("GetReadyWork failed: %v", err) } if len(ready) != 3 { t.Errorf("Expected 3 issues (limit), got %d", len(ready)) } } func TestGetReadyWorkIgnoresRelatedDeps(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create two issues with "related" dependency (should not block) issue1 := &types.Issue{Title: "First", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} issue2 := &types.Issue{Title: "Second", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} store.CreateIssue(ctx, issue1, "test-user") store.CreateIssue(ctx, issue2, "test-user") // Add "related" dependency (not blocking) store.AddDependency(ctx, &types.Dependency{IssueID: issue2.ID, DependsOnID: issue1.ID, Type: types.DepRelated}, "test-user") // Both should be ready (related deps don't block) ready, err := store.GetReadyWork(ctx, types.WorkFilter{Status: types.StatusOpen}) if err != nil { t.Fatalf("GetReadyWork failed: %v", err) } if len(ready) != 2 { t.Fatalf("Expected 2 ready issues (related deps don't block), got %d", len(ready)) } } func TestGetBlockedIssues(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create issues: // bd-1: open, no dependencies → not blocked // bd-2: open, depends on bd-1 (open) → blocked by bd-1 // bd-3: open, depends on bd-1 and bd-2 (both open) → blocked by 2 issues issue1 := &types.Issue{Title: "Foundation", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} issue2 := &types.Issue{Title: "Blocked by 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} issue3 := &types.Issue{Title: "Blocked by 2", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} store.CreateIssue(ctx, issue1, "test-user") store.CreateIssue(ctx, issue2, "test-user") store.CreateIssue(ctx, issue3, "test-user") store.AddDependency(ctx, &types.Dependency{IssueID: issue2.ID, DependsOnID: issue1.ID, Type: types.DepBlocks}, "test-user") store.AddDependency(ctx, &types.Dependency{IssueID: issue3.ID, DependsOnID: issue1.ID, Type: types.DepBlocks}, "test-user") store.AddDependency(ctx, &types.Dependency{IssueID: issue3.ID, DependsOnID: issue2.ID, Type: types.DepBlocks}, "test-user") // Get blocked issues blocked, err := store.GetBlockedIssues(ctx) if err != nil { t.Fatalf("GetBlockedIssues failed: %v", err) } if len(blocked) != 2 { t.Fatalf("Expected 2 blocked issues, got %d", len(blocked)) } // Find issue3 in blocked list var issue3Blocked *types.BlockedIssue for i := range blocked { if blocked[i].ID == issue3.ID { issue3Blocked = blocked[i] break } } if issue3Blocked == nil { t.Fatal("Expected issue3 to be in blocked list") } if issue3Blocked.BlockedByCount != 2 { t.Errorf("Expected issue3 to be blocked by 2 issues, got %d", issue3Blocked.BlockedByCount) } // Verify the blockers are correct if len(issue3Blocked.BlockedBy) != 2 { t.Errorf("Expected 2 blocker IDs, got %d", len(issue3Blocked.BlockedBy)) } } // TestParentBlockerBlocksChildren tests that children inherit blockage from parents func TestParentBlockerBlocksChildren(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create: // blocker: open // epic1: open, blocked by 'blocker' // task1: open, child of epic1 (via parent-child) // // Expected: task1 should NOT be ready (parent is blocked) blocker := &types.Issue{Title: "Blocker", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} epic1 := &types.Issue{Title: "Epic 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeEpic} task1 := &types.Issue{Title: "Task 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} store.CreateIssue(ctx, blocker, "test-user") store.CreateIssue(ctx, epic1, "test-user") store.CreateIssue(ctx, task1, "test-user") // epic1 blocked by blocker store.AddDependency(ctx, &types.Dependency{IssueID: epic1.ID, DependsOnID: blocker.ID, Type: types.DepBlocks}, "test-user") // task1 is child of epic1 store.AddDependency(ctx, &types.Dependency{IssueID: task1.ID, DependsOnID: epic1.ID, Type: types.DepParentChild}, "test-user") // Get ready work ready, err := store.GetReadyWork(ctx, types.WorkFilter{Status: types.StatusOpen}) if err != nil { t.Fatalf("GetReadyWork failed: %v", err) } // Should have only blocker ready readyIDs := make(map[string]bool) for _, issue := range ready { readyIDs[issue.ID] = true } if readyIDs[epic1.ID] { t.Errorf("Expected epic1 to be blocked, but it was ready") } if readyIDs[task1.ID] { t.Errorf("Expected task1 to be blocked (parent is blocked), but it was ready") } if !readyIDs[blocker.ID] { t.Errorf("Expected blocker to be ready") } } // TestGrandparentBlockerBlocksGrandchildren tests multi-level propagation func TestGrandparentBlockerBlocksGrandchildren(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create: // blocker: open // epic1: open, blocked by 'blocker' // epic2: open, child of epic1 // task1: open, child of epic2 // // Expected: task1 should NOT be ready (grandparent is blocked) blocker := &types.Issue{Title: "Blocker", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} epic1 := &types.Issue{Title: "Epic 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeEpic} epic2 := &types.Issue{Title: "Epic 2", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeEpic} task1 := &types.Issue{Title: "Task 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} store.CreateIssue(ctx, blocker, "test-user") store.CreateIssue(ctx, epic1, "test-user") store.CreateIssue(ctx, epic2, "test-user") store.CreateIssue(ctx, task1, "test-user") // epic1 blocked by blocker store.AddDependency(ctx, &types.Dependency{IssueID: epic1.ID, DependsOnID: blocker.ID, Type: types.DepBlocks}, "test-user") // epic2 is child of epic1 store.AddDependency(ctx, &types.Dependency{IssueID: epic2.ID, DependsOnID: epic1.ID, Type: types.DepParentChild}, "test-user") // task1 is child of epic2 store.AddDependency(ctx, &types.Dependency{IssueID: task1.ID, DependsOnID: epic2.ID, Type: types.DepParentChild}, "test-user") // Get ready work ready, err := store.GetReadyWork(ctx, types.WorkFilter{Status: types.StatusOpen}) if err != nil { t.Fatalf("GetReadyWork failed: %v", err) } // Should have only blocker ready readyIDs := make(map[string]bool) for _, issue := range ready { readyIDs[issue.ID] = true } if readyIDs[epic1.ID] { t.Errorf("Expected epic1 to be blocked, but it was ready") } if readyIDs[epic2.ID] { t.Errorf("Expected epic2 to be blocked (parent is blocked), but it was ready") } if readyIDs[task1.ID] { t.Errorf("Expected task1 to be blocked (grandparent is blocked), but it was ready") } if !readyIDs[blocker.ID] { t.Errorf("Expected blocker to be ready") } } // TestMultipleParentsOneBlocked tests that a child is blocked if ANY parent is blocked func TestMultipleParentsOneBlocked(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create: // blocker: open // epic1: open, blocked by 'blocker' // epic2: open, no blockers // task1: open, child of BOTH epic1 and epic2 // // Expected: task1 should NOT be ready (one parent is blocked) blocker := &types.Issue{Title: "Blocker", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} epic1 := &types.Issue{Title: "Epic 1 (blocked)", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeEpic} epic2 := &types.Issue{Title: "Epic 2 (ready)", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeEpic} task1 := &types.Issue{Title: "Task 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} store.CreateIssue(ctx, blocker, "test-user") store.CreateIssue(ctx, epic1, "test-user") store.CreateIssue(ctx, epic2, "test-user") store.CreateIssue(ctx, task1, "test-user") // epic1 blocked by blocker store.AddDependency(ctx, &types.Dependency{IssueID: epic1.ID, DependsOnID: blocker.ID, Type: types.DepBlocks}, "test-user") // task1 is child of both epic1 and epic2 store.AddDependency(ctx, &types.Dependency{IssueID: task1.ID, DependsOnID: epic1.ID, Type: types.DepParentChild}, "test-user") store.AddDependency(ctx, &types.Dependency{IssueID: task1.ID, DependsOnID: epic2.ID, Type: types.DepParentChild}, "test-user") // Get ready work ready, err := store.GetReadyWork(ctx, types.WorkFilter{Status: types.StatusOpen}) if err != nil { t.Fatalf("GetReadyWork failed: %v", err) } // Should have blocker and epic2 ready, but NOT epic1 or task1 readyIDs := make(map[string]bool) for _, issue := range ready { readyIDs[issue.ID] = true } if readyIDs[epic1.ID] { t.Errorf("Expected epic1 to be blocked, but it was ready") } if readyIDs[task1.ID] { t.Errorf("Expected task1 to be blocked (one parent is blocked), but it was ready") } if !readyIDs[blocker.ID] { t.Errorf("Expected blocker to be ready") } if !readyIDs[epic2.ID] { t.Errorf("Expected epic2 to be ready") } } // TestBlockerClosedUnblocksChildren tests that closing a blocker unblocks descendants func TestBlockerClosedUnblocksChildren(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create: // blocker: initially open, then closed // epic1: open, blocked by 'blocker' // task1: open, child of epic1 // // After closing blocker: both epic1 and task1 should be ready blocker := &types.Issue{Title: "Blocker", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} epic1 := &types.Issue{Title: "Epic 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeEpic} task1 := &types.Issue{Title: "Task 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} store.CreateIssue(ctx, blocker, "test-user") store.CreateIssue(ctx, epic1, "test-user") store.CreateIssue(ctx, task1, "test-user") // epic1 blocked by blocker store.AddDependency(ctx, &types.Dependency{IssueID: epic1.ID, DependsOnID: blocker.ID, Type: types.DepBlocks}, "test-user") // task1 is child of epic1 store.AddDependency(ctx, &types.Dependency{IssueID: task1.ID, DependsOnID: epic1.ID, Type: types.DepParentChild}, "test-user") // Initially, epic1 and task1 should be blocked ready, err := store.GetReadyWork(ctx, types.WorkFilter{Status: types.StatusOpen}) if err != nil { t.Fatalf("GetReadyWork failed: %v", err) } readyIDs := make(map[string]bool) for _, issue := range ready { readyIDs[issue.ID] = true } if readyIDs[epic1.ID] || readyIDs[task1.ID] { t.Errorf("Expected epic1 and task1 to be blocked initially") } // Close the blocker store.CloseIssue(ctx, blocker.ID, "Done", "test-user") // Now epic1 and task1 should be ready ready, err = store.GetReadyWork(ctx, types.WorkFilter{Status: types.StatusOpen}) if err != nil { t.Fatalf("GetReadyWork failed after closing blocker: %v", err) } readyIDs = make(map[string]bool) for _, issue := range ready { readyIDs[issue.ID] = true } if !readyIDs[epic1.ID] { t.Errorf("Expected epic1 to be ready after blocker closed") } if !readyIDs[task1.ID] { t.Errorf("Expected task1 to be ready after blocker closed") } } // TestRelatedDoesNotPropagate tests that 'related' deps don't cause blocking propagation func TestRelatedDoesNotPropagate(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create: // blocker: open // epic1: open, blocked by 'blocker' // task1: open, related to epic1 (NOT parent-child) // // Expected: task1 SHOULD be ready (related doesn't propagate blocking) blocker := &types.Issue{Title: "Blocker", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} epic1 := &types.Issue{Title: "Epic 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeEpic} task1 := &types.Issue{Title: "Task 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} store.CreateIssue(ctx, blocker, "test-user") store.CreateIssue(ctx, epic1, "test-user") store.CreateIssue(ctx, task1, "test-user") // epic1 blocked by blocker store.AddDependency(ctx, &types.Dependency{IssueID: epic1.ID, DependsOnID: blocker.ID, Type: types.DepBlocks}, "test-user") // task1 is related to epic1 (NOT parent-child) store.AddDependency(ctx, &types.Dependency{IssueID: task1.ID, DependsOnID: epic1.ID, Type: types.DepRelated}, "test-user") // Get ready work ready, err := store.GetReadyWork(ctx, types.WorkFilter{Status: types.StatusOpen}) if err != nil { t.Fatalf("GetReadyWork failed: %v", err) } // Should have blocker AND task1 ready (related doesn't propagate) readyIDs := make(map[string]bool) for _, issue := range ready { readyIDs[issue.ID] = true } if readyIDs[epic1.ID] { t.Errorf("Expected epic1 to be blocked, but it was ready") } if !readyIDs[task1.ID] { t.Errorf("Expected task1 to be ready (related deps don't propagate blocking), but it was blocked") } if !readyIDs[blocker.ID] { t.Errorf("Expected blocker to be ready") } } // TestCompositeIndexExists verifies the composite index is created func TestCompositeIndexExists(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Query sqlite_master to check if the index exists var indexName string err := store.db.QueryRowContext(ctx, ` SELECT name FROM sqlite_master WHERE type='index' AND name='idx_dependencies_depends_on_type' `).Scan(&indexName) if err != nil { t.Fatalf("Composite index idx_dependencies_depends_on_type not found: %v", err) } if indexName != "idx_dependencies_depends_on_type" { t.Errorf("Expected index name 'idx_dependencies_depends_on_type', got '%s'", indexName) } } // TestReadyIssuesViewMatchesGetReadyWork verifies the ready_issues VIEW produces same results as GetReadyWork func TestReadyIssuesViewMatchesGetReadyWork(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create hierarchy: blocker → epic1 → task1 blocker := &types.Issue{Title: "Blocker", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} epic1 := &types.Issue{Title: "Epic 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeEpic} task1 := &types.Issue{Title: "Task 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} task2 := &types.Issue{Title: "Task 2", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} store.CreateIssue(ctx, blocker, "test-user") store.CreateIssue(ctx, epic1, "test-user") store.CreateIssue(ctx, task1, "test-user") store.CreateIssue(ctx, task2, "test-user") // epic1 blocked by blocker store.AddDependency(ctx, &types.Dependency{IssueID: epic1.ID, DependsOnID: blocker.ID, Type: types.DepBlocks}, "test-user") // task1 is child of epic1 (should be blocked) store.AddDependency(ctx, &types.Dependency{IssueID: task1.ID, DependsOnID: epic1.ID, Type: types.DepParentChild}, "test-user") // task2 has no dependencies (should be ready) // Get ready work via GetReadyWork function ready, err := store.GetReadyWork(ctx, types.WorkFilter{Status: types.StatusOpen}) if err != nil { t.Fatalf("GetReadyWork failed: %v", err) } readyIDsFromFunc := make(map[string]bool) for _, issue := range ready { readyIDsFromFunc[issue.ID] = true } // Get ready work via VIEW rows, err := store.db.QueryContext(ctx, `SELECT id FROM ready_issues ORDER BY id`) if err != nil { t.Fatalf("Query ready_issues VIEW failed: %v", err) } defer rows.Close() readyIDsFromView := make(map[string]bool) for rows.Next() { var id string if err := rows.Scan(&id); err != nil { t.Fatalf("Scan failed: %v", err) } readyIDsFromView[id] = true } // Verify they match if len(readyIDsFromFunc) != len(readyIDsFromView) { t.Errorf("Mismatch: GetReadyWork returned %d issues, VIEW returned %d", len(readyIDsFromFunc), len(readyIDsFromView)) } for id := range readyIDsFromFunc { if !readyIDsFromView[id] { t.Errorf("Issue %s in GetReadyWork but NOT in VIEW", id) } } for id := range readyIDsFromView { if !readyIDsFromFunc[id] { t.Errorf("Issue %s in VIEW but NOT in GetReadyWork", id) } } // Verify specific expectations if !readyIDsFromView[blocker.ID] { t.Errorf("Expected blocker to be ready in VIEW") } if !readyIDsFromView[task2.ID] { t.Errorf("Expected task2 to be ready in VIEW") } if readyIDsFromView[epic1.ID] { t.Errorf("Expected epic1 to be blocked in VIEW (has blocker)") } if readyIDsFromView[task1.ID] { t.Errorf("Expected task1 to be blocked in VIEW (parent is blocked)") } } // TestDeepHierarchyBlocking tests blocking propagation through 50-level deep hierarchy func TestDeepHierarchyBlocking(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create a blocker at the root blocker := &types.Issue{Title: "Root Blocker", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask} store.CreateIssue(ctx, blocker, "test-user") // Create 50-level hierarchy: root → level1 → level2 → ... → level50 var issues []*types.Issue for i := 0; i < 50; i++ { issue := &types.Issue{ Title: "Level " + string(rune(i)), Status: types.StatusOpen, Priority: 1, IssueType: types.TypeEpic, } store.CreateIssue(ctx, issue, "test-user") issues = append(issues, issue) if i == 0 { // First level: blocked by blocker store.AddDependency(ctx, &types.Dependency{ IssueID: issue.ID, DependsOnID: blocker.ID, Type: types.DepBlocks, }, "test-user") } else { // Each subsequent level: child of previous level store.AddDependency(ctx, &types.Dependency{ IssueID: issue.ID, DependsOnID: issues[i-1].ID, Type: types.DepParentChild, }, "test-user") } } // Get ready work ready, err := store.GetReadyWork(ctx, types.WorkFilter{Status: types.StatusOpen}) if err != nil { t.Fatalf("GetReadyWork failed: %v", err) } // Build set of ready IDs readyIDs := make(map[string]bool) for _, issue := range ready { readyIDs[issue.ID] = true } // Only the blocker should be ready if len(ready) != 1 { t.Errorf("Expected exactly 1 ready issue (the blocker), got %d", len(ready)) } if !readyIDs[blocker.ID] { t.Errorf("Expected blocker to be ready") } // All 50 levels should be blocked for i, issue := range issues { if readyIDs[issue.ID] { t.Errorf("Expected level %d (issue %s) to be blocked, but it was ready", i, issue.ID) } } // Now close the blocker and verify all levels become ready store.CloseIssue(ctx, blocker.ID, "Done", "test-user") ready, err = store.GetReadyWork(ctx, types.WorkFilter{Status: types.StatusOpen}) if err != nil { t.Fatalf("GetReadyWork failed after closing blocker: %v", err) } // All 50 levels should now be ready if len(ready) != 50 { t.Errorf("Expected 50 ready issues after closing blocker, got %d", len(ready)) } readyIDs = make(map[string]bool) for _, issue := range ready { readyIDs[issue.ID] = true } for i, issue := range issues { if !readyIDs[issue.ID] { t.Errorf("Expected level %d (issue %s) to be ready after blocker closed, but it was blocked", i, issue.ID) } } }