feat: implement --max-depth flag for bd dep tree (closes #87, bd-3, bd-159)

- Add --max-depth/-d flag with default of 50
- Wire flag through to store.GetDependencyTree()
- Add input validation (must be >= 1)
- Show inline '… [truncated]' markers on truncated nodes
- Update truncation warning to show actual depth used
- Add comprehensive tests (truncation, default depth, boundary cases)
- Update CLI docs and reference

Thanks to @yashwanth-reddy909 for the initial implementation in PR #87.
This commit completes the feature with full wiring, validation, tests, and docs.

Amp-Thread-ID: https://ampcode.com/threads/T-c439b09c-cff2-48d9-8988-cf9353f0d32e
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-10-19 09:00:11 -07:00
parent 22daa12665
commit b0fba2eef2
5 changed files with 180 additions and 6 deletions

View File

@@ -2,6 +2,7 @@ package sqlite
import (
"context"
"fmt"
"strings"
"testing"
@@ -234,6 +235,161 @@ func TestGetDependencyTree(t *testing.T) {
}
}
func TestGetDependencyTree_TruncationDepth(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create a long chain: bd-5 → bd-4 → bd-3 → bd-2 → bd-1
issues := make([]*types.Issue, 5)
for i := 0; i < 5; i++ {
issues[i] = &types.Issue{
Title: fmt.Sprintf("Level %d", i),
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
err := store.CreateIssue(ctx, issues[i], "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
}
// Link them in chain
for i := 1; i < 5; i++ {
err := store.AddDependency(ctx, &types.Dependency{
IssueID: issues[i].ID,
DependsOnID: issues[i-1].ID,
Type: types.DepBlocks,
}, "test-user")
if err != nil {
t.Fatalf("AddDependency failed: %v", err)
}
}
// Get tree with maxDepth=2 (should only get 3 nodes: depths 0, 1, 2)
tree, err := store.GetDependencyTree(ctx, issues[4].ID, 2, false)
if err != nil {
t.Fatalf("GetDependencyTree failed: %v", err)
}
if len(tree) != 3 {
t.Fatalf("Expected 3 nodes with maxDepth=2, got %d", len(tree))
}
// Check that last node is marked as truncated
foundTruncated := false
for _, node := range tree {
if node.Depth == 2 && node.Truncated {
foundTruncated = true
break
}
}
if !foundTruncated {
t.Error("Expected node at depth 2 to be marked as truncated")
}
}
func TestGetDependencyTree_DefaultDepth(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create a simple chain
issue1 := &types.Issue{Title: "Level 0", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issue2 := &types.Issue{Title: "Level 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
store.CreateIssue(ctx, issue1, "test-user")
store.CreateIssue(ctx, issue2, "test-user")
store.AddDependency(ctx, &types.Dependency{
IssueID: issue2.ID,
DependsOnID: issue1.ID,
Type: types.DepBlocks,
}, "test-user")
// Get tree with default depth (50)
tree, err := store.GetDependencyTree(ctx, issue2.ID, 50, false)
if err != nil {
t.Fatalf("GetDependencyTree failed: %v", err)
}
if len(tree) != 2 {
t.Fatalf("Expected 2 nodes, got %d", len(tree))
}
// No truncation should occur
for _, node := range tree {
if node.Truncated {
t.Error("Expected no truncation with default depth on short chain")
}
}
}
func TestGetDependencyTree_MaxDepthOne(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create a chain: bd-3 → bd-2 → bd-1
issue1 := &types.Issue{Title: "Level 2", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issue2 := &types.Issue{Title: "Level 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issue3 := &types.Issue{Title: "Root", 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: issue2.ID,
Type: types.DepBlocks,
}, "test-user")
// Get tree with maxDepth=1 (should get root + one level)
tree, err := store.GetDependencyTree(ctx, issue3.ID, 1, false)
if err != nil {
t.Fatalf("GetDependencyTree failed: %v", err)
}
// Should get root (depth 0) and one child (depth 1)
if len(tree) != 2 {
t.Fatalf("Expected 2 nodes with maxDepth=1, got %d", len(tree))
}
// Check root is at depth 0 and not truncated
rootFound := false
for _, node := range tree {
if node.ID == issue3.ID && node.Depth == 0 && !node.Truncated {
rootFound = true
}
}
if !rootFound {
t.Error("Expected root at depth 0, not truncated")
}
// Check child at depth 1 is truncated
childTruncated := false
for _, node := range tree {
if node.Depth == 1 && node.Truncated {
childTruncated = true
}
}
if !childTruncated {
t.Error("Expected child at depth 1 to be truncated")
}
}
func TestDetectCycles(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()