Fix tests (bd-6ss and sub-issues) (#626)

Test coverage improvements for bd-6ss. Fixing failing test assumption in follow-up commit.
This commit is contained in:
matt wilkie
2025-12-18 19:23:30 -07:00
committed by GitHub
parent 7f9ee3d1c4
commit fb16e504e6
14 changed files with 1656 additions and 10 deletions

View File

@@ -1215,3 +1215,396 @@ func TestGetDependenciesWithMetadataMultipleTypes(t *testing.T) {
t.Errorf("Expected discovered dependency type 'discovered-from', got %s", typeMap[discovered.ID])
}
}
// TestGetDependencyTree_ComplexDiamond tests a diamond dependency pattern
func TestGetDependencyTree_ComplexDiamond(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create diamond pattern:
// D
// / \
// B C
// \ /
// A
issueA := &types.Issue{Title: "A", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issueB := &types.Issue{Title: "B", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issueC := &types.Issue{Title: "C", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issueD := &types.Issue{Title: "D", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
store.CreateIssue(ctx, issueA, "test-user")
store.CreateIssue(ctx, issueB, "test-user")
store.CreateIssue(ctx, issueC, "test-user")
store.CreateIssue(ctx, issueD, "test-user")
// Create dependencies: D blocks B, D blocks C, B blocks A, C blocks A
store.AddDependency(ctx, &types.Dependency{IssueID: issueB.ID, DependsOnID: issueD.ID, Type: types.DepBlocks}, "test-user")
store.AddDependency(ctx, &types.Dependency{IssueID: issueC.ID, DependsOnID: issueD.ID, Type: types.DepBlocks}, "test-user")
store.AddDependency(ctx, &types.Dependency{IssueID: issueA.ID, DependsOnID: issueB.ID, Type: types.DepBlocks}, "test-user")
store.AddDependency(ctx, &types.Dependency{IssueID: issueA.ID, DependsOnID: issueC.ID, Type: types.DepBlocks}, "test-user")
// Get tree from A
tree, err := store.GetDependencyTree(ctx, issueA.ID, 50, false, false)
if err != nil {
t.Fatalf("GetDependencyTree failed: %v", err)
}
// Should have all 4 nodes
if len(tree) != 4 {
t.Fatalf("Expected 4 nodes in diamond, got %d", len(tree))
}
// Verify all expected nodes are present
idSet := make(map[string]bool)
for _, node := range tree {
idSet[node.ID] = true
}
expected := []string{issueA.ID, issueB.ID, issueC.ID, issueD.ID}
for _, id := range expected {
if !idSet[id] {
t.Errorf("Expected node %s in diamond tree", id)
}
}
}
// TestGetDependencyTree_ShowAllPaths tests the showAllPaths flag behavior
func TestGetDependencyTree_ShowAllPaths(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create diamond again
issueA := &types.Issue{Title: "A", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issueB := &types.Issue{Title: "B", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issueC := &types.Issue{Title: "C", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issueD := &types.Issue{Title: "D", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
store.CreateIssue(ctx, issueA, "test-user")
store.CreateIssue(ctx, issueB, "test-user")
store.CreateIssue(ctx, issueC, "test-user")
store.CreateIssue(ctx, issueD, "test-user")
store.AddDependency(ctx, &types.Dependency{IssueID: issueB.ID, DependsOnID: issueD.ID, Type: types.DepBlocks}, "test-user")
store.AddDependency(ctx, &types.Dependency{IssueID: issueC.ID, DependsOnID: issueD.ID, Type: types.DepBlocks}, "test-user")
store.AddDependency(ctx, &types.Dependency{IssueID: issueA.ID, DependsOnID: issueB.ID, Type: types.DepBlocks}, "test-user")
store.AddDependency(ctx, &types.Dependency{IssueID: issueA.ID, DependsOnID: issueC.ID, Type: types.DepBlocks}, "test-user")
// Get tree with showAllPaths=true
treeAll, err := store.GetDependencyTree(ctx, issueA.ID, 50, true, false)
if err != nil {
t.Fatalf("GetDependencyTree with showAllPaths failed: %v", err)
}
// Get tree with showAllPaths=false
treeDedup, err := store.GetDependencyTree(ctx, issueA.ID, 50, false, false)
if err != nil {
t.Fatalf("GetDependencyTree without showAllPaths failed: %v", err)
}
// Both should have at least the core nodes
if len(treeAll) < len(treeDedup) {
t.Errorf("showAllPaths=true should have >= nodes than showAllPaths=false: got %d vs %d", len(treeAll), len(treeDedup))
}
}
// TestGetDependencyTree_ReverseDirection tests getting dependents instead of dependencies
func TestGetDependencyTree_ReverseDirection(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create a chain: A depends on B, B depends on C
// So: B blocks A, C blocks B
// Normal (down): From A we get [A, B, C] (dependencies)
// Reverse (up): From C we get [C, B, A] (dependents)
issueA := &types.Issue{Title: "A", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issueB := &types.Issue{Title: "B", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issueC := &types.Issue{Title: "C", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
store.CreateIssue(ctx, issueA, "test-user")
store.CreateIssue(ctx, issueB, "test-user")
store.CreateIssue(ctx, issueC, "test-user")
// A depends on B, B depends on C
store.AddDependency(ctx, &types.Dependency{IssueID: issueA.ID, DependsOnID: issueB.ID, Type: types.DepBlocks}, "test-user")
store.AddDependency(ctx, &types.Dependency{IssueID: issueB.ID, DependsOnID: issueC.ID, Type: types.DepBlocks}, "test-user")
// Get normal tree from A (should get A as root, then dependencies B, C)
downTree, err := store.GetDependencyTree(ctx, issueA.ID, 50, false, false)
if err != nil {
t.Fatalf("GetDependencyTree down failed: %v", err)
}
// Get reverse tree from C (should get C as root, then dependents B, A)
upTree, err := store.GetDependencyTree(ctx, issueC.ID, 50, false, true)
if err != nil {
t.Fatalf("GetDependencyTree reverse failed: %v", err)
}
// Both should include their root nodes
if len(downTree) < 1 {
t.Fatal("Down tree should include at least the root node A")
}
if len(upTree) < 1 {
t.Fatal("Up tree should include at least the root node C")
}
// Down tree should start with A at depth 0
if downTree[0].ID != issueA.ID {
t.Errorf("Down tree should start with A, got %s", downTree[0].ID)
}
// Up tree should start with C at depth 0
if upTree[0].ID != issueC.ID {
t.Errorf("Up tree should start with C, got %s", upTree[0].ID)
}
// Down tree from A should have B and C as dependencies
downHasB := false
downHasC := false
for _, node := range downTree {
if node.ID == issueB.ID {
downHasB = true
}
if node.ID == issueC.ID {
downHasC = true
}
}
if !downHasB || !downHasC {
t.Error("Down tree from A should include B and C as dependencies")
}
// Up tree from C should have B and A as dependents
upHasB := false
upHasA := false
for _, node := range upTree {
if node.ID == issueB.ID {
upHasB = true
}
if node.ID == issueA.ID {
upHasA = true
}
}
if !upHasB || !upHasA {
t.Error("Up tree from C should include B and A as dependents")
}
}
// TestDetectCycles_SingleCyclePrevention verifies single-issue cycles are caught
func TestDetectCycles_PreventionAtAddTime(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create two issues
issueA := &types.Issue{Title: "A", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issueB := &types.Issue{Title: "B", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
store.CreateIssue(ctx, issueA, "test-user")
store.CreateIssue(ctx, issueB, "test-user")
// Add A -> B
err := store.AddDependency(ctx, &types.Dependency{
IssueID: issueA.ID,
DependsOnID: issueB.ID,
Type: types.DepBlocks,
}, "test-user")
if err != nil {
t.Fatalf("First AddDependency failed: %v", err)
}
// Try to add B -> A (would create cycle) - should fail
err = store.AddDependency(ctx, &types.Dependency{
IssueID: issueB.ID,
DependsOnID: issueA.ID,
Type: types.DepBlocks,
}, "test-user")
if err == nil {
t.Fatal("Expected AddDependency to fail when creating 2-node cycle")
}
// Verify no cycles exist
cycles, err := store.DetectCycles(ctx)
if err != nil {
t.Fatalf("DetectCycles failed: %v", err)
}
if len(cycles) != 0 {
t.Error("Expected no cycles since cycle was prevented at add time")
}
}
// TestDetectCycles_LongerCycle tests detection of longer cycles
func TestDetectCycles_LongerCyclePrevention(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create a chain: A -> B -> C
issues := make(map[string]*types.Issue)
for _, name := range []string{"A", "B", "C"} {
issue := &types.Issue{Title: name, Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
store.CreateIssue(ctx, issue, "test-user")
issues[name] = issue
}
// Build chain A -> B -> C
store.AddDependency(ctx, &types.Dependency{
IssueID: issues["A"].ID,
DependsOnID: issues["B"].ID,
Type: types.DepBlocks,
}, "test-user")
store.AddDependency(ctx, &types.Dependency{
IssueID: issues["B"].ID,
DependsOnID: issues["C"].ID,
Type: types.DepBlocks,
}, "test-user")
// Try to close the cycle: C -> A (should fail)
err := store.AddDependency(ctx, &types.Dependency{
IssueID: issues["C"].ID,
DependsOnID: issues["A"].ID,
Type: types.DepBlocks,
}, "test-user")
if err == nil {
t.Fatal("Expected AddDependency to fail when creating 3-node cycle")
}
// Verify no cycles
cycles, err := store.DetectCycles(ctx)
if err != nil {
t.Fatalf("DetectCycles failed: %v", err)
}
if len(cycles) != 0 {
t.Error("Expected no cycles since cycle was prevented")
}
}
// TestDetectCycles_MultipleIndependentGraphs tests cycles in isolated subgraphs
func TestDetectCycles_MultipleGraphs(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create two independent dependency chains
// Chain 1: A1 -> B1 -> C1
// Chain 2: A2 -> B2 -> C2
chains := [][]string{{"A1", "B1", "C1"}, {"A2", "B2", "C2"}}
issuesMap := make(map[string]*types.Issue)
for _, chain := range chains {
for _, name := range chain {
issue := &types.Issue{Title: name, Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
store.CreateIssue(ctx, issue, "test-user")
issuesMap[name] = issue
}
// Link the chain
for i := 0; i < len(chain)-1; i++ {
store.AddDependency(ctx, &types.Dependency{
IssueID: issuesMap[chain[i]].ID,
DependsOnID: issuesMap[chain[i+1]].ID,
Type: types.DepBlocks,
}, "test-user")
}
}
// Verify no cycles
cycles, err := store.DetectCycles(ctx)
if err != nil {
t.Fatalf("DetectCycles failed: %v", err)
}
if len(cycles) != 0 {
t.Errorf("Expected no cycles in independent chains, got %d", len(cycles))
}
}
// TestDetectCycles_RelatedTypeAllowsAdditionButReportsDetection tests relates-to allows bidirectional links
// even though they're technically cycles. The cycle prevention only skips relates-to during AddDependency.
func TestDetectCycles_RelatedTypeAllowsAdditionButReportsDetection(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create two issues
issueA := &types.Issue{Title: "A", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issueB := &types.Issue{Title: "B", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
store.CreateIssue(ctx, issueA, "test-user")
store.CreateIssue(ctx, issueB, "test-user")
// Add A relates-to B
err := store.AddDependency(ctx, &types.Dependency{
IssueID: issueA.ID,
DependsOnID: issueB.ID,
Type: types.DepRelatesTo,
}, "test-user")
if err != nil {
t.Fatalf("AddDependency for relates-to failed: %v", err)
}
// Add B relates-to A (this should succeed despite creating a cycle because relates-to skips cycle detection)
err = store.AddDependency(ctx, &types.Dependency{
IssueID: issueB.ID,
DependsOnID: issueA.ID,
Type: types.DepRelatesTo,
}, "test-user")
if err != nil {
t.Fatalf("AddDependency for reverse relates-to failed: %v", err)
}
// DetectCycles will report the cycle even though AddDependency allowed it
cycles, err := store.DetectCycles(ctx)
if err != nil {
t.Fatalf("DetectCycles failed: %v", err)
}
// Relates-to bidirectional creates cycles (may report multiple entry points for same cycle)
if len(cycles) == 0 {
t.Error("Expected at least 1 cycle detected for bidirectional relates-to")
}
// Verify both directions exist
depsA, err := store.GetDependenciesWithMetadata(ctx, issueA.ID)
if err != nil {
t.Fatalf("GetDependenciesWithMetadata failed: %v", err)
}
depsB, err := store.GetDependenciesWithMetadata(ctx, issueB.ID)
if err != nil {
t.Fatalf("GetDependenciesWithMetadata failed: %v", err)
}
// A should have B as a dependency
hasB := false
for _, dep := range depsA {
if dep.ID == issueB.ID && dep.DependencyType == types.DepRelatesTo {
hasB = true
break
}
}
if !hasB {
t.Error("Expected A to relate-to B")
}
// B should have A as a dependency
hasA := false
for _, dep := range depsB {
if dep.ID == issueA.ID && dep.DependencyType == types.DepRelatesTo {
hasA = true
break
}
}
if !hasA {
t.Error("Expected B to relate-to A")
}
}