Files
beads/cmd/bd/validate_test.go
Steve Yegge 76069a09dc Improve cmd/bd test coverage from 21% to 23.1% (bd-27ea)
Added comprehensive tests for:
- validate.go (parseChecks, validation results, orphaned deps, duplicates, git conflicts)
- restore.go (readIssueFromJSONL, git helpers)
- sync.go (git helpers, JSONL counting)

Progress: 21.0% → 23.1% (+2.1%)
Target: 40% (multi-session effort)
Amp-Thread-ID: https://ampcode.com/threads/T-540ebf64-e14f-4541-b098-586d2b07dc3e
Co-authored-by: Amp <amp@ampcode.com>
2025-10-31 19:44:43 -07:00

351 lines
7.7 KiB
Go

package main
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/steveyegge/beads/internal/types"
)
func TestParseChecks(t *testing.T) {
tests := []struct {
name string
input string
want []string
wantError bool
}{
{
name: "empty returns all defaults",
input: "",
want: []string{"orphans", "duplicates", "pollution", "conflicts"},
},
{
name: "single check",
input: "orphans",
want: []string{"orphans"},
},
{
name: "multiple checks",
input: "orphans,duplicates",
want: []string{"orphans", "duplicates"},
},
{
name: "synonym dupes->duplicates",
input: "dupes",
want: []string{"duplicates"},
},
{
name: "synonym git-conflicts->conflicts",
input: "git-conflicts",
want: []string{"conflicts"},
},
{
name: "mixed with whitespace",
input: " orphans , duplicates , pollution ",
want: []string{"orphans", "duplicates", "pollution"},
},
{
name: "deduplication",
input: "orphans,orphans,duplicates",
want: []string{"orphans", "duplicates"},
},
{
name: "invalid check",
input: "orphans,invalid,duplicates",
wantError: true,
},
{
name: "empty parts ignored",
input: "orphans,,duplicates",
want: []string{"orphans", "duplicates"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseChecks(tt.input)
if tt.wantError {
if err == nil {
t.Errorf("parseChecks(%q) expected error, got nil", tt.input)
}
return
}
if err != nil {
t.Fatalf("parseChecks(%q) unexpected error: %v", tt.input, err)
}
if len(got) != len(tt.want) {
t.Errorf("parseChecks(%q) length = %d, want %d", tt.input, len(got), len(tt.want))
return
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("parseChecks(%q)[%d] = %q, want %q", tt.input, i, got[i], tt.want[i])
}
}
})
}
}
func TestValidationResultsHasFailures(t *testing.T) {
tests := []struct {
name string
checks map[string]checkResult
want bool
}{
{
name: "no failures - all clean",
checks: map[string]checkResult{
"orphans": {issueCount: 0, fixedCount: 0},
"dupes": {issueCount: 0, fixedCount: 0},
},
want: false,
},
{
name: "has error",
checks: map[string]checkResult{
"orphans": {err: os.ErrNotExist},
},
want: true,
},
{
name: "issues found but not all fixed",
checks: map[string]checkResult{
"orphans": {issueCount: 5, fixedCount: 3},
},
want: true,
},
{
name: "issues found and all fixed",
checks: map[string]checkResult{
"orphans": {issueCount: 5, fixedCount: 5},
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &validationResults{checks: tt.checks}
got := r.hasFailures()
if got != tt.want {
t.Errorf("hasFailures() = %v, want %v", got, tt.want)
}
})
}
}
func TestValidationResultsToJSON(t *testing.T) {
r := &validationResults{
checks: map[string]checkResult{
"orphans": {
issueCount: 3,
fixedCount: 2,
suggestions: []string{"Run bd repair"},
},
"dupes": {
issueCount: 0,
fixedCount: 0,
},
},
}
output := r.toJSON()
if output["total_issues"] != 3 {
t.Errorf("total_issues = %v, want 3", output["total_issues"])
}
if output["total_fixed"] != 2 {
t.Errorf("total_fixed = %v, want 2", output["total_fixed"])
}
if output["healthy"] != false {
t.Errorf("healthy = %v, want false", output["healthy"])
}
checks := output["checks"].(map[string]interface{})
orphans := checks["orphans"].(map[string]interface{})
if orphans["issue_count"] != 3 {
t.Errorf("orphans issue_count = %v, want 3", orphans["issue_count"])
}
if orphans["fixed_count"] != 2 {
t.Errorf("orphans fixed_count = %v, want 2", orphans["fixed_count"])
}
}
func TestValidateOrphanedDeps(t *testing.T) {
ctx := context.Background()
allIssues := []*types.Issue{
{
ID: "bd-1",
Dependencies: []*types.Dependency{
{DependsOnID: "bd-2", Type: types.DepBlocks},
{DependsOnID: "bd-999", Type: types.DepBlocks}, // orphaned
},
},
{
ID: "bd-2",
},
}
result := validateOrphanedDeps(ctx, allIssues, false)
if result.issueCount != 1 {
t.Errorf("issueCount = %d, want 1", result.issueCount)
}
if result.fixedCount != 0 {
t.Errorf("fixedCount = %d, want 0 (fix=false)", result.fixedCount)
}
if len(result.suggestions) == 0 {
t.Error("expected suggestions")
}
}
func TestValidateDuplicates(t *testing.T) {
ctx := context.Background()
allIssues := []*types.Issue{
{
ID: "bd-1",
Title: "Same title",
},
{
ID: "bd-2",
Title: "Same title",
},
{
ID: "bd-3",
Title: "Different",
},
}
result := validateDuplicates(ctx, allIssues, false)
// Should find 1 duplicate (bd-2 is duplicate of bd-1)
if result.issueCount != 1 {
t.Errorf("issueCount = %d, want 1", result.issueCount)
}
if len(result.suggestions) == 0 {
t.Error("expected suggestions")
}
}
func TestValidatePollution(t *testing.T) {
ctx := context.Background()
allIssues := []*types.Issue{
{
ID: "test-1",
Title: "Test issue",
},
{
ID: "bd-1",
Title: "Normal issue",
},
}
result := validatePollution(ctx, allIssues, false)
// Should detect test-1 as pollution
if result.issueCount != 1 {
t.Errorf("issueCount = %d, want 1", result.issueCount)
}
}
func TestValidateGitConflicts_NoFile(t *testing.T) {
ctx := context.Background()
// Create temp dir without JSONL
tmpDir := t.TempDir()
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatal(err)
}
// Override dbPath to point to temp dir
originalDBPath := dbPath
dbPath = filepath.Join(beadsDir, "beads.db")
defer func() { dbPath = originalDBPath }()
result := validateGitConflicts(ctx, false)
if result.issueCount != 0 {
t.Errorf("issueCount = %d, want 0 (no file)", result.issueCount)
}
if result.err != nil {
t.Errorf("unexpected error: %v", result.err)
}
}
func TestValidateGitConflicts_WithMarkers(t *testing.T) {
ctx := context.Background()
// Create temp JSONL with conflict markers
tmpDir := t.TempDir()
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatal(err)
}
jsonlPath := filepath.Join(beadsDir, "beads.jsonl")
content := `{"id":"bd-1"}
<<<<<<< HEAD
{"id":"bd-2","title":"Version A"}
=======
{"id":"bd-2","title":"Version B"}
>>>>>>> main
{"id":"bd-3"}`
if err := os.WriteFile(jsonlPath, []byte(content), 0644); err != nil {
t.Fatal(err)
}
// Override dbPath to point to temp dir
originalDBPath := dbPath
dbPath = filepath.Join(beadsDir, "beads.db")
defer func() { dbPath = originalDBPath }()
result := validateGitConflicts(ctx, false)
if result.issueCount != 1 {
t.Errorf("issueCount = %d, want 1 (conflict found)", result.issueCount)
}
if len(result.suggestions) == 0 {
t.Error("expected suggestions for conflict resolution")
}
}
func TestValidateGitConflicts_Clean(t *testing.T) {
ctx := context.Background()
// Create temp JSONL without conflicts
tmpDir := t.TempDir()
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatal(err)
}
jsonlPath := filepath.Join(beadsDir, "beads.jsonl")
content := `{"id":"bd-1","title":"Normal"}
{"id":"bd-2","title":"Also normal"}`
if err := os.WriteFile(jsonlPath, []byte(content), 0644); err != nil {
t.Fatal(err)
}
// Override dbPath to point to temp dir
originalDBPath := dbPath
dbPath = filepath.Join(beadsDir, "beads.db")
defer func() { dbPath = originalDBPath }()
result := validateGitConflicts(ctx, false)
if result.issueCount != 0 {
t.Errorf("issueCount = %d, want 0 (clean file)", result.issueCount)
}
if result.err != nil {
t.Errorf("unexpected error: %v", result.err)
}
}