Files
beads/internal/storage/dolt/dependencies_extended_test.go
groblegark 9d64be56cf test(dolt): add comprehensive test suite for Dolt storage backend (#1299)
Add extensive test coverage for the Dolt storage implementation:

- dependencies_extended_test.go: Extended dependency operation tests
- dolt_benchmark_test.go: Performance benchmarks for Dolt operations
- history_test.go: Version history query tests
- labels_test.go: Label operation tests

These tests validate Dolt backend correctness and provide performance
baselines for comparison with SQLite.

Co-authored-by: upstream_syncer <matthew.baker@pihealth.ai>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 17:12:01 -08:00

581 lines
18 KiB
Go

//go:build cgo
package dolt
import (
"testing"
"github.com/steveyegge/beads/internal/types"
)
// =============================================================================
// GetDependenciesWithMetadata Tests
// =============================================================================
func TestGetDependenciesWithMetadata(t *testing.T) {
// Note: This test is skipped in embedded Dolt mode because GetDependenciesWithMetadata
// makes nested GetIssue calls inside a rows cursor, which can cause connection issues.
// This is a known limitation of the current implementation (see bd-tdgo.3).
t.Skip("Skipping: GetDependenciesWithMetadata has nested query issue in embedded Dolt mode")
}
func TestGetDependenciesWithMetadata_NoResults(t *testing.T) {
store, cleanup := setupTestStore(t)
defer cleanup()
ctx, cancel := testContext(t)
defer cancel()
// Create an issue with no dependencies
issue := &types.Issue{
ID: "no-deps-issue",
Title: "No Dependencies",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, issue, "tester"); err != nil {
t.Fatalf("failed to create issue: %v", err)
}
deps, err := store.GetDependenciesWithMetadata(ctx, issue.ID)
if err != nil {
t.Fatalf("GetDependenciesWithMetadata failed: %v", err)
}
if len(deps) != 0 {
t.Errorf("expected 0 dependencies, got %d", len(deps))
}
}
// =============================================================================
// GetDependentsWithMetadata Tests
// =============================================================================
func TestGetDependentsWithMetadata(t *testing.T) {
// Note: This test is skipped in embedded Dolt mode because GetDependentsWithMetadata
// makes nested GetIssue calls inside a rows cursor, which can cause connection issues.
// This is a known limitation of the current implementation (see bd-tdgo.3).
t.Skip("Skipping: GetDependentsWithMetadata has nested query issue in embedded Dolt mode")
}
// =============================================================================
// GetDependencyRecords Tests
// =============================================================================
func TestGetDependencyRecords(t *testing.T) {
store, cleanup := setupTestStore(t)
defer cleanup()
ctx, cancel := testContext(t)
defer cancel()
// Create issues
issue := &types.Issue{
ID: "records-main",
Title: "Main Issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, issue, "tester"); err != nil {
t.Fatalf("failed to create issue: %v", err)
}
dep1 := &types.Issue{
ID: "records-dep1",
Title: "Dependency 1",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, dep1, "tester"); err != nil {
t.Fatalf("failed to create dep1: %v", err)
}
dep2 := &types.Issue{
ID: "records-dep2",
Title: "Dependency 2",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, dep2, "tester"); err != nil {
t.Fatalf("failed to create dep2: %v", err)
}
// Add dependencies
for _, depIssue := range []string{dep1.ID, dep2.ID} {
d := &types.Dependency{
IssueID: issue.ID,
DependsOnID: depIssue,
Type: types.DepBlocks,
}
if err := store.AddDependency(ctx, d, "tester"); err != nil {
t.Fatalf("failed to add dependency: %v", err)
}
}
// Get dependency records
records, err := store.GetDependencyRecords(ctx, issue.ID)
if err != nil {
t.Fatalf("GetDependencyRecords failed: %v", err)
}
if len(records) != 2 {
t.Fatalf("expected 2 records, got %d", len(records))
}
// Verify structure
for _, r := range records {
if r.IssueID != issue.ID {
t.Errorf("expected IssueID %q, got %q", issue.ID, r.IssueID)
}
if r.Type != types.DepBlocks {
t.Errorf("expected type %q, got %q", types.DepBlocks, r.Type)
}
if r.CreatedBy != "tester" {
t.Errorf("expected CreatedBy 'tester', got %q", r.CreatedBy)
}
}
}
// =============================================================================
// GetAllDependencyRecords Tests
// =============================================================================
func TestGetAllDependencyRecords(t *testing.T) {
store, cleanup := setupTestStore(t)
defer cleanup()
ctx, cancel := testContext(t)
defer cancel()
// Create several issues with dependencies
issueA := &types.Issue{ID: "all-deps-a", Title: "Issue A", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issueB := &types.Issue{ID: "all-deps-b", Title: "Issue B", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issueC := &types.Issue{ID: "all-deps-c", Title: "Issue C", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
for _, issue := range []*types.Issue{issueA, issueB, issueC} {
if err := store.CreateIssue(ctx, issue, "tester"); err != nil {
t.Fatalf("failed to create issue: %v", err)
}
}
// A depends on B, B depends on C
deps := []*types.Dependency{
{IssueID: issueA.ID, DependsOnID: issueB.ID, Type: types.DepBlocks},
{IssueID: issueB.ID, DependsOnID: issueC.ID, Type: types.DepBlocks},
}
for _, d := range deps {
if err := store.AddDependency(ctx, d, "tester"); err != nil {
t.Fatalf("failed to add dependency: %v", err)
}
}
// Get all dependency records
allRecords, err := store.GetAllDependencyRecords(ctx)
if err != nil {
t.Fatalf("GetAllDependencyRecords failed: %v", err)
}
// Should have records keyed by issueA and issueB
if len(allRecords) < 2 {
t.Errorf("expected at least 2 issues with dependencies, got %d", len(allRecords))
}
if _, ok := allRecords[issueA.ID]; !ok {
t.Errorf("expected records for %q", issueA.ID)
}
if _, ok := allRecords[issueB.ID]; !ok {
t.Errorf("expected records for %q", issueB.ID)
}
}
// =============================================================================
// GetDependencyCounts Tests
// =============================================================================
func TestGetDependencyCounts(t *testing.T) {
store, cleanup := setupTestStore(t)
defer cleanup()
ctx, cancel := testContext(t)
defer cancel()
// Create issues: root blocks mid1 and mid2, mid1 blocks leaf
root := &types.Issue{ID: "counts-root", Title: "Root", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
mid1 := &types.Issue{ID: "counts-mid1", Title: "Mid 1", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask}
mid2 := &types.Issue{ID: "counts-mid2", Title: "Mid 2", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask}
leaf := &types.Issue{ID: "counts-leaf", Title: "Leaf", Status: types.StatusOpen, Priority: 3, IssueType: types.TypeTask}
for _, issue := range []*types.Issue{root, mid1, mid2, leaf} {
if err := store.CreateIssue(ctx, issue, "tester"); err != nil {
t.Fatalf("failed to create issue: %v", err)
}
}
// Add blocking dependencies
deps := []*types.Dependency{
{IssueID: mid1.ID, DependsOnID: root.ID, Type: types.DepBlocks}, // mid1 blocked by root
{IssueID: mid2.ID, DependsOnID: root.ID, Type: types.DepBlocks}, // mid2 blocked by root
{IssueID: leaf.ID, DependsOnID: mid1.ID, Type: types.DepBlocks}, // leaf blocked by mid1
}
for _, d := range deps {
if err := store.AddDependency(ctx, d, "tester"); err != nil {
t.Fatalf("failed to add dependency: %v", err)
}
}
// Get counts for all issues
issueIDs := []string{root.ID, mid1.ID, mid2.ID, leaf.ID}
counts, err := store.GetDependencyCounts(ctx, issueIDs)
if err != nil {
t.Fatalf("GetDependencyCounts failed: %v", err)
}
// root: 0 deps, 2 dependents
if counts[root.ID].DependencyCount != 0 {
t.Errorf("root should have 0 deps, got %d", counts[root.ID].DependencyCount)
}
if counts[root.ID].DependentCount != 2 {
t.Errorf("root should have 2 dependents, got %d", counts[root.ID].DependentCount)
}
// mid1: 1 dep, 1 dependent
if counts[mid1.ID].DependencyCount != 1 {
t.Errorf("mid1 should have 1 dep, got %d", counts[mid1.ID].DependencyCount)
}
if counts[mid1.ID].DependentCount != 1 {
t.Errorf("mid1 should have 1 dependent, got %d", counts[mid1.ID].DependentCount)
}
// leaf: 1 dep, 0 dependents
if counts[leaf.ID].DependencyCount != 1 {
t.Errorf("leaf should have 1 dep, got %d", counts[leaf.ID].DependencyCount)
}
if counts[leaf.ID].DependentCount != 0 {
t.Errorf("leaf should have 0 dependents, got %d", counts[leaf.ID].DependentCount)
}
}
func TestGetDependencyCounts_EmptyList(t *testing.T) {
store, cleanup := setupTestStore(t)
defer cleanup()
ctx, cancel := testContext(t)
defer cancel()
counts, err := store.GetDependencyCounts(ctx, []string{})
if err != nil {
t.Fatalf("GetDependencyCounts failed: %v", err)
}
if len(counts) != 0 {
t.Errorf("expected empty map, got %d entries", len(counts))
}
}
// =============================================================================
// GetDependencyTree Tests
// =============================================================================
func TestGetDependencyTree(t *testing.T) {
store, cleanup := setupTestStore(t)
defer cleanup()
ctx, cancel := testContext(t)
defer cancel()
// Create a simple tree: root -> child1, root -> child2
root := &types.Issue{ID: "tree-root", Title: "Root", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeEpic}
child1 := &types.Issue{ID: "tree-child1", Title: "Child 1", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask}
child2 := &types.Issue{ID: "tree-child2", Title: "Child 2", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask}
for _, issue := range []*types.Issue{root, child1, child2} {
if err := store.CreateIssue(ctx, issue, "tester"); err != nil {
t.Fatalf("failed to create issue: %v", err)
}
}
// Add dependencies
for _, childID := range []string{child1.ID, child2.ID} {
d := &types.Dependency{
IssueID: root.ID,
DependsOnID: childID,
Type: types.DepBlocks,
}
if err := store.AddDependency(ctx, d, "tester"); err != nil {
t.Fatalf("failed to add dependency: %v", err)
}
}
// Get dependency tree (forward direction)
tree, err := store.GetDependencyTree(ctx, root.ID, 3, false, false)
if err != nil {
t.Fatalf("GetDependencyTree failed: %v", err)
}
// Should have root + 2 children = 3 nodes
if len(tree) != 3 {
t.Errorf("expected 3 nodes in tree, got %d", len(tree))
}
// Verify root is at depth 0
if tree[0].Issue.ID != root.ID || tree[0].Depth != 0 {
t.Errorf("expected root at depth 0, got %q at depth %d", tree[0].Issue.ID, tree[0].Depth)
}
}
func TestGetDependencyTree_Reverse(t *testing.T) {
store, cleanup := setupTestStore(t)
defer cleanup()
ctx, cancel := testContext(t)
defer cancel()
// Create chain: leaf -> mid -> root
root := &types.Issue{ID: "rtree-root", Title: "Root", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
mid := &types.Issue{ID: "rtree-mid", Title: "Mid", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask}
leaf := &types.Issue{ID: "rtree-leaf", Title: "Leaf", Status: types.StatusOpen, Priority: 3, IssueType: types.TypeTask}
for _, issue := range []*types.Issue{root, mid, leaf} {
if err := store.CreateIssue(ctx, issue, "tester"); err != nil {
t.Fatalf("failed to create issue: %v", err)
}
}
// mid depends on root, leaf depends on mid
deps := []*types.Dependency{
{IssueID: mid.ID, DependsOnID: root.ID, Type: types.DepBlocks},
{IssueID: leaf.ID, DependsOnID: mid.ID, Type: types.DepBlocks},
}
for _, d := range deps {
if err := store.AddDependency(ctx, d, "tester"); err != nil {
t.Fatalf("failed to add dependency: %v", err)
}
}
// Get reverse tree from root (shows dependents)
tree, err := store.GetDependencyTree(ctx, root.ID, 3, false, true)
if err != nil {
t.Fatalf("GetDependencyTree reverse failed: %v", err)
}
// Should have root + mid = 2 nodes (leaf is dependent of mid, not root)
if len(tree) < 2 {
t.Errorf("expected at least 2 nodes in reverse tree, got %d", len(tree))
}
}
// =============================================================================
// DetectCycles Tests
// =============================================================================
func TestDetectCycles_NoCycle(t *testing.T) {
store, cleanup := setupTestStore(t)
defer cleanup()
ctx, cancel := testContext(t)
defer cancel()
// Create linear chain: A -> B -> C
issueA := &types.Issue{ID: "nocycle-a", Title: "A", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issueB := &types.Issue{ID: "nocycle-b", Title: "B", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issueC := &types.Issue{ID: "nocycle-c", Title: "C", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
for _, issue := range []*types.Issue{issueA, issueB, issueC} {
if err := store.CreateIssue(ctx, issue, "tester"); err != nil {
t.Fatalf("failed to create issue: %v", err)
}
}
// A depends on B, B depends on C
deps := []*types.Dependency{
{IssueID: issueA.ID, DependsOnID: issueB.ID, Type: types.DepBlocks},
{IssueID: issueB.ID, DependsOnID: issueC.ID, Type: types.DepBlocks},
}
for _, d := range deps {
if err := store.AddDependency(ctx, d, "tester"); err != nil {
t.Fatalf("failed to add dependency: %v", err)
}
}
cycles, err := store.DetectCycles(ctx)
if err != nil {
t.Fatalf("DetectCycles failed: %v", err)
}
if len(cycles) != 0 {
t.Errorf("expected no cycles, found %d", len(cycles))
}
}
func TestDetectCycles_WithCycle(t *testing.T) {
store, cleanup := setupTestStore(t)
defer cleanup()
ctx, cancel := testContext(t)
defer cancel()
// Create cycle: A -> B -> C -> A
issueA := &types.Issue{ID: "cycle-a", Title: "A", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issueB := &types.Issue{ID: "cycle-b", Title: "B", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
issueC := &types.Issue{ID: "cycle-c", Title: "C", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
for _, issue := range []*types.Issue{issueA, issueB, issueC} {
if err := store.CreateIssue(ctx, issue, "tester"); err != nil {
t.Fatalf("failed to create issue: %v", err)
}
}
// Create cycle
deps := []*types.Dependency{
{IssueID: issueA.ID, DependsOnID: issueB.ID, Type: types.DepBlocks},
{IssueID: issueB.ID, DependsOnID: issueC.ID, Type: types.DepBlocks},
{IssueID: issueC.ID, DependsOnID: issueA.ID, Type: types.DepBlocks}, // Creates cycle
}
for _, d := range deps {
if err := store.AddDependency(ctx, d, "tester"); err != nil {
t.Fatalf("failed to add dependency: %v", err)
}
}
cycles, err := store.DetectCycles(ctx)
if err != nil {
t.Fatalf("DetectCycles failed: %v", err)
}
if len(cycles) == 0 {
t.Error("expected to find a cycle")
}
}
// =============================================================================
// GetNewlyUnblockedByClose Tests
// =============================================================================
func TestGetNewlyUnblockedByClose(t *testing.T) {
store, cleanup := setupTestStore(t)
defer cleanup()
ctx, cancel := testContext(t)
defer cancel()
// Create blocker and two blocked issues
blocker := &types.Issue{
ID: "unblock-blocker",
Title: "Blocker",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
blockedOnly := &types.Issue{
ID: "unblock-only",
Title: "Blocked Only by One",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
blockedMultiple := &types.Issue{
ID: "unblock-multi",
Title: "Blocked by Multiple",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
otherBlocker := &types.Issue{
ID: "unblock-other",
Title: "Other Blocker",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
for _, issue := range []*types.Issue{blocker, blockedOnly, blockedMultiple, otherBlocker} {
if err := store.CreateIssue(ctx, issue, "tester"); err != nil {
t.Fatalf("failed to create issue: %v", err)
}
}
// blockedOnly depends only on blocker
// blockedMultiple depends on both blocker and otherBlocker
deps := []*types.Dependency{
{IssueID: blockedOnly.ID, DependsOnID: blocker.ID, Type: types.DepBlocks},
{IssueID: blockedMultiple.ID, DependsOnID: blocker.ID, Type: types.DepBlocks},
{IssueID: blockedMultiple.ID, DependsOnID: otherBlocker.ID, Type: types.DepBlocks},
}
for _, d := range deps {
if err := store.AddDependency(ctx, d, "tester"); err != nil {
t.Fatalf("failed to add dependency: %v", err)
}
}
// Get issues that would be unblocked if we close 'blocker'
unblocked, err := store.GetNewlyUnblockedByClose(ctx, blocker.ID)
if err != nil {
t.Fatalf("GetNewlyUnblockedByClose failed: %v", err)
}
// Only blockedOnly should be newly unblocked (blockedMultiple still has otherBlocker)
if len(unblocked) != 1 {
t.Fatalf("expected 1 newly unblocked issue, got %d", len(unblocked))
}
if unblocked[0].ID != blockedOnly.ID {
t.Errorf("expected %q to be unblocked, got %q", blockedOnly.ID, unblocked[0].ID)
}
}
func TestGetNewlyUnblockedByClose_ClosedDependent(t *testing.T) {
store, cleanup := setupTestStore(t)
defer cleanup()
ctx, cancel := testContext(t)
defer cancel()
// Create issues where the dependent is already closed
blocker := &types.Issue{
ID: "unblock-closed-blocker",
Title: "Blocker",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
closedDependent := &types.Issue{
ID: "unblock-closed-dep",
Title: "Already Closed",
Status: types.StatusClosed,
Priority: 2,
IssueType: types.TypeTask,
}
for _, issue := range []*types.Issue{blocker, closedDependent} {
if err := store.CreateIssue(ctx, issue, "tester"); err != nil {
t.Fatalf("failed to create issue: %v", err)
}
}
// Add dependency
dep := &types.Dependency{
IssueID: closedDependent.ID,
DependsOnID: blocker.ID,
Type: types.DepBlocks,
}
if err := store.AddDependency(ctx, dep, "tester"); err != nil {
t.Fatalf("failed to add dependency: %v", err)
}
// Closed issues should not be returned as newly unblocked
unblocked, err := store.GetNewlyUnblockedByClose(ctx, blocker.ID)
if err != nil {
t.Fatalf("GetNewlyUnblockedByClose failed: %v", err)
}
if len(unblocked) != 0 {
t.Errorf("expected 0 unblocked (closed issue shouldn't count), got %d", len(unblocked))
}
}
// Note: testContext is already defined in dolt_test.go for this package