feat: add hierarchical tree display for --tree --parent combination (#1211)

Motivation:
The existing --parent flag only shows direct children in a flat list,
but users often need to see the complete hierarchy including grandchildren
and deeper levels. This limitation made it difficult to understand the
full scope of work under an epic or parent issue.

Key changes:
- Enhanced list command to detect --tree --parent combination
- Implemented recursive parent filtering instead of GetDependencyTree
- Added DRY refactoring with withStorage() and getHierarchicalChildren() helpers
- Eliminated duplication between daemon and direct modes
- Added comprehensive test coverage with TestHierarchicalChildren
- Fixed cross-repository compatibility issues

Side-effects:
- No breaking changes: existing --parent behavior unchanged
- --tree --parent now shows hierarchical tree instead of flat list
- Parent issue is included as root of the displayed tree
- Works consistently across all repositories and storage modes
- Improved code maintainability with DRY architecture
- Better test coverage ensures reliability and prevents regressions
This commit is contained in:
Oliver Jägle
2026-01-20 23:06:17 +01:00
committed by GitHub
parent ee44498659
commit d929c8f974
2 changed files with 232 additions and 0 deletions

View File

@@ -801,3 +801,94 @@ func TestListTimeBasedFilters(t *testing.T) {
}
})
}
// TestHierarchicalChildren tests the --tree --parent functionality for showing all descendants
func TestHierarchicalChildren(t *testing.T) {
tmpDir := t.TempDir()
testDB := filepath.Join(tmpDir, ".beads", "beads.db")
store := newTestStore(t, testDB)
ctx := context.Background()
// Helper to create issue
createIssue := func(title string, issueType types.IssueType) *types.Issue {
issue := &types.Issue{
Title: title,
Priority: 2,
IssueType: issueType,
Status: types.StatusOpen,
}
if err := store.CreateIssue(ctx, issue, "test-user"); err != nil {
t.Fatalf("Failed to create issue %s: %v", title, err)
}
return issue
}
// Helper to add dependency
addDep := func(child, parent *types.Issue) {
dep := &types.Dependency{
IssueID: child.ID,
DependsOnID: parent.ID,
Type: types.DepParentChild,
CreatedAt: time.Now(),
CreatedBy: "test-user",
}
if err := store.AddDependency(ctx, dep, "test-user"); err != nil {
t.Fatalf("Failed to add dependency %s -> %s: %v", child.ID, parent.ID, err)
}
}
// Create test hierarchy: Parent -> Child1 (-> Grandchild1.1, Grandchild1.2) + Child2 (-> Grandchild2.1)
parent := createIssue("Parent Epic", types.TypeEpic)
child1 := createIssue("Child 1", types.TypeTask)
child2 := createIssue("Child 2", types.TypeTask)
grandchild11 := createIssue("Grandchild 1.1", types.TypeTask)
grandchild12 := createIssue("Grandchild 1.2", types.TypeTask)
grandchild21 := createIssue("Grandchild 2.1", types.TypeTask)
addDep(child1, parent)
addDep(child2, parent)
addDep(grandchild11, child1)
addDep(grandchild12, child1)
addDep(grandchild21, child2)
// Test full hierarchy (should return all 6 issues)
t.Run("full_hierarchy", func(t *testing.T) {
issues, err := getHierarchicalChildren(ctx, store, "", 0, parent.ID)
if err != nil {
t.Fatalf("getHierarchicalChildren failed: %v", err)
}
if len(issues) != 6 {
t.Errorf("Expected 6 issues in hierarchy, got %d", len(issues))
}
})
// Test child subset (should return child1 + its 2 grandchildren = 3 total)
t.Run("child_subset", func(t *testing.T) {
issues, err := getHierarchicalChildren(ctx, store, "", 0, child1.ID)
if err != nil {
t.Fatalf("getHierarchicalChildren for child1 failed: %v", err)
}
if len(issues) != 3 {
t.Errorf("Expected 3 issues in child1 hierarchy, got %d", len(issues))
}
})
// Test leaf node (should return only itself)
t.Run("leaf_node", func(t *testing.T) {
issues, err := getHierarchicalChildren(ctx, store, "", 0, grandchild11.ID)
if err != nil {
t.Fatalf("getHierarchicalChildren for leaf failed: %v", err)
}
if len(issues) != 1 || issues[0].ID != grandchild11.ID {
t.Errorf("Expected 1 issue (leaf), got %d", len(issues))
}
})
// Test error case - non-existent parent
t.Run("nonexistent_parent", func(t *testing.T) {
_, err := getHierarchicalChildren(ctx, store, "", 0, "nonexistent-id")
if err == nil || !strings.Contains(err.Error(), "not found") {
t.Error("Expected 'not found' error for nonexistent parent")
}
})
}