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>
This commit is contained in:
File diff suppressed because one or more lines are too long
155
cmd/bd/restore_test.go
Normal file
155
cmd/bd/restore_test.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/steveyegge/beads/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadIssueFromJSONL(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
jsonlPath := filepath.Join(tmpDir, "test.jsonl")
|
||||||
|
|
||||||
|
// Create test JSONL file
|
||||||
|
issues := []*types.Issue{
|
||||||
|
{ID: "bd-1", Title: "First issue"},
|
||||||
|
{ID: "bd-2", Title: "Second issue"},
|
||||||
|
{ID: "bd-3", Title: "Third issue"},
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(jsonlPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, issue := range issues {
|
||||||
|
data, _ := json.Marshal(issue)
|
||||||
|
file.Write(data)
|
||||||
|
file.WriteString("\n")
|
||||||
|
}
|
||||||
|
file.Close()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
issueID string
|
||||||
|
wantTitle string
|
||||||
|
wantNil bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "find existing issue",
|
||||||
|
issueID: "bd-2",
|
||||||
|
wantTitle: "Second issue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "issue not found",
|
||||||
|
issueID: "bd-999",
|
||||||
|
wantNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find first issue",
|
||||||
|
issueID: "bd-1",
|
||||||
|
wantTitle: "First issue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find last issue",
|
||||||
|
issueID: "bd-3",
|
||||||
|
wantTitle: "Third issue",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
issue, err := readIssueFromJSONL(jsonlPath, tt.issueID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("readIssueFromJSONL() error = %v", err)
|
||||||
|
}
|
||||||
|
if tt.wantNil {
|
||||||
|
if issue != nil {
|
||||||
|
t.Errorf("expected nil, got issue %s", issue.ID)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if issue == nil {
|
||||||
|
t.Fatal("expected issue, got nil")
|
||||||
|
}
|
||||||
|
if issue.Title != tt.wantTitle {
|
||||||
|
t.Errorf("Title = %q, want %q", issue.Title, tt.wantTitle)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadIssueFromJSONL_NonExistentFile(t *testing.T) {
|
||||||
|
_, err := readIssueFromJSONL("/nonexistent/path.jsonl", "bd-1")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for nonexistent file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadIssueFromJSONL_MalformedJSON(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
jsonlPath := filepath.Join(tmpDir, "malformed.jsonl")
|
||||||
|
|
||||||
|
// Create JSONL with mix of valid and malformed entries
|
||||||
|
content := `{"id":"bd-1","title":"First"}
|
||||||
|
{this is not valid json}
|
||||||
|
{"id":"bd-2","title":"Second"}
|
||||||
|
incomplete line without brace
|
||||||
|
{"id":"bd-3","title":"Third"}`
|
||||||
|
|
||||||
|
if err := os.WriteFile(jsonlPath, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should still find valid entries, skipping malformed ones
|
||||||
|
issue, err := readIssueFromJSONL(jsonlPath, "bd-2")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if issue == nil {
|
||||||
|
t.Fatal("expected to find bd-2 despite malformed lines")
|
||||||
|
}
|
||||||
|
if issue.Title != "Second" {
|
||||||
|
t.Errorf("Title = %q, want %q", issue.Title, "Second")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGitHasUncommittedChanges_NotInGitRepo(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
originalWd, _ := os.Getwd()
|
||||||
|
defer os.Chdir(originalWd)
|
||||||
|
|
||||||
|
os.Chdir(tmpDir)
|
||||||
|
|
||||||
|
// Should error when not in a git repo
|
||||||
|
_, err := gitHasUncommittedChanges()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error when not in git repo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCurrentGitHead_NotInGitRepo(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
originalWd, _ := os.Getwd()
|
||||||
|
defer os.Chdir(originalWd)
|
||||||
|
|
||||||
|
os.Chdir(tmpDir)
|
||||||
|
|
||||||
|
// Should error when not in a git repo
|
||||||
|
_, err := getCurrentGitHead()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error when not in git repo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGitCheckout_InvalidRef(t *testing.T) {
|
||||||
|
// Don't actually test this in real git repo to avoid side effects
|
||||||
|
// Just verify the function signature exists
|
||||||
|
err := gitCheckout("nonexistent-ref-12345")
|
||||||
|
if err == nil {
|
||||||
|
// If we're not in a git repo or ref doesn't exist, should error
|
||||||
|
t.Log("gitCheckout returned nil - might not be in git repo or ref exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
270
cmd/bd/sync_test.go
Normal file
270
cmd/bd/sync_test.go
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsGitRepo_InGitRepo(t *testing.T) {
|
||||||
|
// This test assumes we're running in the beads git repo
|
||||||
|
if !isGitRepo() {
|
||||||
|
t.Skip("not in a git repository")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsGitRepo_NotInGitRepo(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
originalWd, _ := os.Getwd()
|
||||||
|
defer os.Chdir(originalWd)
|
||||||
|
|
||||||
|
os.Chdir(tmpDir)
|
||||||
|
|
||||||
|
if isGitRepo() {
|
||||||
|
t.Error("expected false when not in git repo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGitHasUpstream_NoUpstream(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
originalWd, _ := os.Getwd()
|
||||||
|
defer os.Chdir(originalWd)
|
||||||
|
|
||||||
|
// Create a fresh git repo without upstream
|
||||||
|
os.Chdir(tmpDir)
|
||||||
|
exec.Command("git", "init").Run()
|
||||||
|
exec.Command("git", "config", "user.email", "test@test.com").Run()
|
||||||
|
exec.Command("git", "config", "user.name", "Test User").Run()
|
||||||
|
|
||||||
|
// Create initial commit
|
||||||
|
os.WriteFile("test.txt", []byte("test"), 0644)
|
||||||
|
exec.Command("git", "add", "test.txt").Run()
|
||||||
|
exec.Command("git", "commit", "-m", "initial").Run()
|
||||||
|
|
||||||
|
// Should not have upstream
|
||||||
|
if gitHasUpstream() {
|
||||||
|
t.Error("expected false when no upstream configured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGitHasChanges_NoFile(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
originalWd, _ := os.Getwd()
|
||||||
|
defer os.Chdir(originalWd)
|
||||||
|
|
||||||
|
// Create a git repo
|
||||||
|
os.Chdir(tmpDir)
|
||||||
|
exec.Command("git", "init").Run()
|
||||||
|
exec.Command("git", "config", "user.email", "test@test.com").Run()
|
||||||
|
exec.Command("git", "config", "user.name", "Test User").Run()
|
||||||
|
|
||||||
|
// Create and commit a file
|
||||||
|
testFile := filepath.Join(tmpDir, "test.txt")
|
||||||
|
os.WriteFile(testFile, []byte("original"), 0644)
|
||||||
|
exec.Command("git", "add", "test.txt").Run()
|
||||||
|
exec.Command("git", "commit", "-m", "initial").Run()
|
||||||
|
|
||||||
|
// Check - should have no changes
|
||||||
|
hasChanges, err := gitHasChanges(ctx, "test.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("gitHasChanges() error = %v", err)
|
||||||
|
}
|
||||||
|
if hasChanges {
|
||||||
|
t.Error("expected no changes for committed file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGitHasChanges_ModifiedFile(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
originalWd, _ := os.Getwd()
|
||||||
|
defer os.Chdir(originalWd)
|
||||||
|
|
||||||
|
// Create a git repo
|
||||||
|
os.Chdir(tmpDir)
|
||||||
|
exec.Command("git", "init").Run()
|
||||||
|
exec.Command("git", "config", "user.email", "test@test.com").Run()
|
||||||
|
exec.Command("git", "config", "user.name", "Test User").Run()
|
||||||
|
|
||||||
|
// Create and commit a file
|
||||||
|
testFile := filepath.Join(tmpDir, "test.txt")
|
||||||
|
os.WriteFile(testFile, []byte("original"), 0644)
|
||||||
|
exec.Command("git", "add", "test.txt").Run()
|
||||||
|
exec.Command("git", "commit", "-m", "initial").Run()
|
||||||
|
|
||||||
|
// Modify the file
|
||||||
|
os.WriteFile(testFile, []byte("modified"), 0644)
|
||||||
|
|
||||||
|
// Check - should have changes
|
||||||
|
hasChanges, err := gitHasChanges(ctx, "test.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("gitHasChanges() error = %v", err)
|
||||||
|
}
|
||||||
|
if !hasChanges {
|
||||||
|
t.Error("expected changes for modified file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGitHasUnmergedPaths_CleanRepo(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
originalWd, _ := os.Getwd()
|
||||||
|
defer os.Chdir(originalWd)
|
||||||
|
|
||||||
|
// Create a git repo
|
||||||
|
os.Chdir(tmpDir)
|
||||||
|
exec.Command("git", "init").Run()
|
||||||
|
exec.Command("git", "config", "user.email", "test@test.com").Run()
|
||||||
|
exec.Command("git", "config", "user.name", "Test User").Run()
|
||||||
|
|
||||||
|
// Create initial commit
|
||||||
|
os.WriteFile("test.txt", []byte("test"), 0644)
|
||||||
|
exec.Command("git", "add", "test.txt").Run()
|
||||||
|
exec.Command("git", "commit", "-m", "initial").Run()
|
||||||
|
|
||||||
|
// Should not have unmerged paths
|
||||||
|
hasUnmerged, err := gitHasUnmergedPaths()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("gitHasUnmergedPaths() error = %v", err)
|
||||||
|
}
|
||||||
|
if hasUnmerged {
|
||||||
|
t.Error("expected no unmerged paths in clean repo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGitCommit_Success(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
originalWd, _ := os.Getwd()
|
||||||
|
defer os.Chdir(originalWd)
|
||||||
|
|
||||||
|
// Create a git repo
|
||||||
|
os.Chdir(tmpDir)
|
||||||
|
exec.Command("git", "init").Run()
|
||||||
|
exec.Command("git", "config", "user.email", "test@test.com").Run()
|
||||||
|
exec.Command("git", "config", "user.name", "Test User").Run()
|
||||||
|
|
||||||
|
// Create initial commit
|
||||||
|
os.WriteFile("initial.txt", []byte("initial"), 0644)
|
||||||
|
exec.Command("git", "add", "initial.txt").Run()
|
||||||
|
exec.Command("git", "commit", "-m", "initial").Run()
|
||||||
|
|
||||||
|
// Create a new file
|
||||||
|
testFile := "test.txt"
|
||||||
|
os.WriteFile(testFile, []byte("content"), 0644)
|
||||||
|
|
||||||
|
// Commit the file
|
||||||
|
err := gitCommit(ctx, testFile, "test commit")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("gitCommit() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify file is committed
|
||||||
|
hasChanges, err := gitHasChanges(ctx, testFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("gitHasChanges() error = %v", err)
|
||||||
|
}
|
||||||
|
if hasChanges {
|
||||||
|
t.Error("expected no changes after commit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGitCommit_AutoMessage(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
originalWd, _ := os.Getwd()
|
||||||
|
defer os.Chdir(originalWd)
|
||||||
|
|
||||||
|
// Create a git repo
|
||||||
|
os.Chdir(tmpDir)
|
||||||
|
exec.Command("git", "init").Run()
|
||||||
|
exec.Command("git", "config", "user.email", "test@test.com").Run()
|
||||||
|
exec.Command("git", "config", "user.name", "Test User").Run()
|
||||||
|
|
||||||
|
// Create initial commit
|
||||||
|
os.WriteFile("initial.txt", []byte("initial"), 0644)
|
||||||
|
exec.Command("git", "add", "initial.txt").Run()
|
||||||
|
exec.Command("git", "commit", "-m", "initial").Run()
|
||||||
|
|
||||||
|
// Create a new file
|
||||||
|
testFile := "test.txt"
|
||||||
|
os.WriteFile(testFile, []byte("content"), 0644)
|
||||||
|
|
||||||
|
// Commit with auto-generated message (empty string)
|
||||||
|
err := gitCommit(ctx, testFile, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("gitCommit() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it committed (message generation worked)
|
||||||
|
cmd := exec.Command("git", "log", "-1", "--pretty=%B")
|
||||||
|
output, _ := cmd.Output()
|
||||||
|
if len(output) == 0 {
|
||||||
|
t.Error("expected commit message to be generated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCountIssuesInJSONL_NonExistent(t *testing.T) {
|
||||||
|
count, err := countIssuesInJSONL("/nonexistent/path.jsonl")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for nonexistent file")
|
||||||
|
}
|
||||||
|
if count != 0 {
|
||||||
|
t.Errorf("count = %d, want 0 on error", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCountIssuesInJSONL_EmptyFile(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
jsonlPath := filepath.Join(tmpDir, "empty.jsonl")
|
||||||
|
os.WriteFile(jsonlPath, []byte(""), 0644)
|
||||||
|
|
||||||
|
count, err := countIssuesInJSONL(jsonlPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if count != 0 {
|
||||||
|
t.Errorf("count = %d, want 0", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCountIssuesInJSONL_MultipleIssues(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
jsonlPath := filepath.Join(tmpDir, "issues.jsonl")
|
||||||
|
content := `{"id":"bd-1"}
|
||||||
|
{"id":"bd-2"}
|
||||||
|
{"id":"bd-3"}
|
||||||
|
`
|
||||||
|
os.WriteFile(jsonlPath, []byte(content), 0644)
|
||||||
|
|
||||||
|
count, err := countIssuesInJSONL(jsonlPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if count != 3 {
|
||||||
|
t.Errorf("count = %d, want 3", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCountIssuesInJSONL_WithMalformedLines(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
jsonlPath := filepath.Join(tmpDir, "mixed.jsonl")
|
||||||
|
content := `{"id":"bd-1"}
|
||||||
|
not valid json
|
||||||
|
{"id":"bd-2"}
|
||||||
|
{"id":"bd-3"}
|
||||||
|
`
|
||||||
|
os.WriteFile(jsonlPath, []byte(content), 0644)
|
||||||
|
|
||||||
|
count, err := countIssuesInJSONL(jsonlPath)
|
||||||
|
// countIssuesInJSONL returns error on malformed JSON
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for malformed JSON")
|
||||||
|
}
|
||||||
|
// Should have counted the first valid issue before hitting error
|
||||||
|
if count != 1 {
|
||||||
|
t.Errorf("count = %d, want 1 (before malformed line)", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
350
cmd/bd/validate_test.go
Normal file
350
cmd/bd/validate_test.go
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user