feat: add reverse mode to dependency tree traversal

- Add reverse parameter to GetDependencyTree interface
- Implement reverse SQL traversal (dependents vs dependencies)
- Add comprehensive test for reverse mode (TDD: test-first approach)
- Update existing test calls with reverse=false for backward compatibility

Reverse mode inverts tree direction to show dependents instead of dependencies:
- Normal: JOIN dependencies d ON i.id = d.depends_on_id (traverse UP)
- Reverse: JOIN dependencies d ON i.id = d.issue_id (traverse DOWN)

All storage tests passing. No regressions.
This commit is contained in:
David Laing
2025-10-27 09:17:07 +00:00
parent 969f3ac03b
commit dd8f51c433
3 changed files with 166 additions and 43 deletions

View File

@@ -249,7 +249,7 @@ func TestGetDependencyTree(t *testing.T) {
store.AddDependency(ctx, &types.Dependency{IssueID: issue3.ID, DependsOnID: issue2.ID, Type: types.DepBlocks}, "test-user")
// Get tree starting from issue3
tree, err := store.GetDependencyTree(ctx, issue3.ID, 10, false)
tree, err := store.GetDependencyTree(ctx, issue3.ID, 10, false, false)
if err != nil {
t.Fatalf("GetDependencyTree failed: %v", err)
}
@@ -311,7 +311,7 @@ func TestGetDependencyTree_TruncationDepth(t *testing.T) {
}
// Get tree with maxDepth=2 (should only get 3 nodes: depths 0, 1, 2)
tree, err := store.GetDependencyTree(ctx, issues[4].ID, 2, false)
tree, err := store.GetDependencyTree(ctx, issues[4].ID, 2, false, false)
if err != nil {
t.Fatalf("GetDependencyTree failed: %v", err)
}
@@ -354,7 +354,7 @@ func TestGetDependencyTree_DefaultDepth(t *testing.T) {
}, "test-user")
// Get tree with default depth (50)
tree, err := store.GetDependencyTree(ctx, issue2.ID, 50, false)
tree, err := store.GetDependencyTree(ctx, issue2.ID, 50, false, false)
if err != nil {
t.Fatalf("GetDependencyTree failed: %v", err)
}
@@ -399,7 +399,7 @@ func TestGetDependencyTree_MaxDepthOne(t *testing.T) {
}, "test-user")
// Get tree with maxDepth=1 (should get root + one level)
tree, err := store.GetDependencyTree(ctx, issue3.ID, 1, false)
tree, err := store.GetDependencyTree(ctx, issue3.ID, 1, false, false)
if err != nil {
t.Fatalf("GetDependencyTree failed: %v", err)
}
@@ -725,3 +725,78 @@ func TestCrossTypeCyclePreventionThreeIssues(t *testing.T) {
t.Errorf("Expected no cycles after prevention, but found %d", len(cycles))
}
}
func TestGetDependencyTree_Reverse(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create a dependency chain: issue1 <- issue2 <- issue3
// (issue3 depends on issue2, issue2 depends on issue1)
issue1 := &types.Issue{
Title: "Base issue",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
issue2 := &types.Issue{
Title: "Depends on issue1",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
issue3 := &types.Issue{
Title: "Depends on issue2",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
store.CreateIssue(ctx, issue1, "test")
store.CreateIssue(ctx, issue2, "test")
store.CreateIssue(ctx, issue3, "test")
// Create dependencies: issue3 → issue2 → issue1
dep1 := &types.Dependency{IssueID: issue2.ID, DependsOnID: issue1.ID, Type: types.DepBlocks}
dep2 := &types.Dependency{IssueID: issue3.ID, DependsOnID: issue2.ID, Type: types.DepBlocks}
store.AddDependency(ctx, dep1, "test")
store.AddDependency(ctx, dep2, "test")
// Test normal mode: from issue3, should traverse UP to issue1
normalTree, err := store.GetDependencyTree(ctx, issue3.ID, 10, false, false)
if err != nil {
t.Fatalf("GetDependencyTree normal mode failed: %v", err)
}
if len(normalTree) != 3 {
t.Fatalf("Expected 3 nodes in normal tree, got %d", len(normalTree))
}
// Test reverse mode: from issue1, should traverse DOWN to issue3
reverseTree, err := store.GetDependencyTree(ctx, issue1.ID, 10, false, true)
if err != nil {
t.Fatalf("GetDependencyTree reverse mode failed: %v", err)
}
if len(reverseTree) != 3 {
t.Fatalf("Expected 3 nodes in reverse tree, got %d", len(reverseTree))
}
// Verify reverse tree structure: issue1 at depth 0
depthMap := make(map[string]int)
for _, node := range reverseTree {
depthMap[node.ID] = node.Depth
}
if depthMap[issue1.ID] != 0 {
t.Errorf("Expected depth 0 for %s in reverse tree, got %d", issue1.ID, depthMap[issue1.ID])
}
// issue2 should be at depth 1 (depends on issue1)
if depthMap[issue2.ID] != 1 {
t.Errorf("Expected depth 1 for %s in reverse tree, got %d", issue2.ID, depthMap[issue2.ID])
}
// issue3 should be at depth 2 (depends on issue2)
if depthMap[issue3.ID] != 2 {
t.Errorf("Expected depth 2 for %s in reverse tree, got %d", issue3.ID, depthMap[issue3.ID])
}
}