Files
beads/cmd/bd/dep_test.go
Steve Yegge 9111d1e573 Add test coverage for compact, ready, and dep commands
- Add comprehensive tests for cmd/bd/compact.go
  - Test dry run, validation, stats, progress bar, uptime formatting
  - Test compaction eligibility checks

- Add comprehensive tests for cmd/bd/ready.go
  - Test ready work filtering by priority, assignee, limit
  - Test blocking dependencies exclusion
  - Test in-progress issues inclusion

- Add comprehensive tests for cmd/bd/dep.go
  - Test dependency add/remove operations
  - Test all dependency types (blocks, related, parent-child, discovered-from)
  - Test cycle detection and prevention

Coverage improved from 45.6% to 46.0% overall
cmd/bd coverage improved from 20.0% to 20.4%

Amp-Thread-ID: https://ampcode.com/threads/T-0707eb82-f56e-4b2d-b64a-f18cc5bc7421
Co-authored-by: Amp <amp@ampcode.com>
2025-10-24 16:35:40 -07:00

303 lines
6.5 KiB
Go

package main
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
"time"
"github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types"
)
func TestDepAdd(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, ".beads", "beads.db")
if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil {
t.Fatal(err)
}
sqliteStore, err := sqlite.New(dbPath)
if err != nil {
t.Fatal(err)
}
defer sqliteStore.Close()
ctx := context.Background()
// Create test issues
issues := []*types.Issue{
{
ID: "test-1",
Title: "Task 1",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
CreatedAt: time.Now(),
},
{
ID: "test-2",
Title: "Task 2",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
CreatedAt: time.Now(),
},
}
for _, issue := range issues {
if err := sqliteStore.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatal(err)
}
}
// Add dependency
dep := &types.Dependency{
IssueID: "test-1",
DependsOnID: "test-2",
Type: types.DepBlocks,
CreatedAt: time.Now(),
}
if err := sqliteStore.AddDependency(ctx, dep, "test"); err != nil {
t.Fatalf("AddDependency failed: %v", err)
}
// Verify dependency was added
deps, err := sqliteStore.GetDependencies(ctx, "test-1")
if err != nil {
t.Fatalf("GetDependencies failed: %v", err)
}
if len(deps) != 1 {
t.Fatalf("Expected 1 dependency, got %d", len(deps))
}
if deps[0].ID != "test-2" {
t.Errorf("Expected dependency on test-2, got %s", deps[0].ID)
}
}
func TestDepTypes(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, ".beads", "beads.db")
if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil {
t.Fatal(err)
}
sqliteStore, err := sqlite.New(dbPath)
if err != nil {
t.Fatal(err)
}
defer sqliteStore.Close()
ctx := context.Background()
// Create test issues
for i := 1; i <= 4; i++ {
issue := &types.Issue{
ID: fmt.Sprintf("test-%d", i),
Title: fmt.Sprintf("Task %d", i),
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
CreatedAt: time.Now(),
}
if err := sqliteStore.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatal(err)
}
}
// Test different dependency types (without creating cycles)
depTypes := []struct {
depType types.DependencyType
from string
to string
}{
{types.DepBlocks, "test-2", "test-1"},
{types.DepRelated, "test-3", "test-1"},
{types.DepParentChild, "test-4", "test-1"},
{types.DepDiscoveredFrom, "test-3", "test-2"},
}
for _, dt := range depTypes {
dep := &types.Dependency{
IssueID: dt.from,
DependsOnID: dt.to,
Type: dt.depType,
CreatedAt: time.Now(),
}
if err := sqliteStore.AddDependency(ctx, dep, "test"); err != nil {
t.Fatalf("AddDependency failed for type %s: %v", dt.depType, err)
}
}
}
func TestDepCycleDetection(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, ".beads", "beads.db")
if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil {
t.Fatal(err)
}
sqliteStore, err := sqlite.New(dbPath)
if err != nil {
t.Fatal(err)
}
defer sqliteStore.Close()
ctx := context.Background()
// Create test issues
for i := 1; i <= 3; i++ {
issue := &types.Issue{
ID: fmt.Sprintf("test-%d", i),
Title: fmt.Sprintf("Task %d", i),
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
CreatedAt: time.Now(),
}
if err := sqliteStore.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatal(err)
}
}
// Create a cycle: test-1 -> test-2 -> test-3 -> test-1
// Add first two deps successfully
deps := []struct {
from string
to string
}{
{"test-1", "test-2"},
{"test-2", "test-3"},
}
for _, d := range deps {
dep := &types.Dependency{
IssueID: d.from,
DependsOnID: d.to,
Type: types.DepBlocks,
CreatedAt: time.Now(),
}
if err := sqliteStore.AddDependency(ctx, dep, "test"); err != nil {
t.Fatalf("AddDependency failed: %v", err)
}
}
// Try to add the third dep which would create a cycle - should fail
cycleDep := &types.Dependency{
IssueID: "test-3",
DependsOnID: "test-1",
Type: types.DepBlocks,
CreatedAt: time.Now(),
}
if err := sqliteStore.AddDependency(ctx, cycleDep, "test"); err == nil {
t.Fatal("Expected AddDependency to fail when creating cycle, but it succeeded")
}
// Since cycle detection prevented the cycle, DetectCycles should find no cycles
cycles, err := sqliteStore.DetectCycles(ctx)
if err != nil {
t.Fatalf("DetectCycles failed: %v", err)
}
if len(cycles) != 0 {
t.Error("Expected no cycles since cycle was prevented")
}
}
func TestDepCommandsInit(t *testing.T) {
if depCmd == nil {
t.Fatal("depCmd should be initialized")
}
if depCmd.Use != "dep" {
t.Errorf("Expected Use='dep', got %q", depCmd.Use)
}
if depAddCmd == nil {
t.Fatal("depAddCmd should be initialized")
}
if depRemoveCmd == nil {
t.Fatal("depRemoveCmd should be initialized")
}
}
func TestDepRemove(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, ".beads", "beads.db")
if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil {
t.Fatal(err)
}
sqliteStore, err := sqlite.New(dbPath)
if err != nil {
t.Fatal(err)
}
defer sqliteStore.Close()
ctx := context.Background()
// Create test issues
issues := []*types.Issue{
{
ID: "test-1",
Title: "Task 1",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
CreatedAt: time.Now(),
},
{
ID: "test-2",
Title: "Task 2",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
CreatedAt: time.Now(),
},
}
for _, issue := range issues {
if err := sqliteStore.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatal(err)
}
}
// Add dependency
dep := &types.Dependency{
IssueID: "test-1",
DependsOnID: "test-2",
Type: types.DepBlocks,
CreatedAt: time.Now(),
}
if err := sqliteStore.AddDependency(ctx, dep, "test"); err != nil {
t.Fatal(err)
}
// Remove dependency
if err := sqliteStore.RemoveDependency(ctx, "test-1", "test-2", "test"); err != nil {
t.Fatalf("RemoveDependency failed: %v", err)
}
// Verify dependency was removed
deps, err := sqliteStore.GetDependencies(ctx, "test-1")
if err != nil {
t.Fatalf("GetDependencies failed: %v", err)
}
if len(deps) != 0 {
t.Errorf("Expected 0 dependencies after removal, got %d", len(deps))
}
}