Files
beads/cmd/bd/duplicates_test.go
Steve Yegge 6f3b0e7c99 doc: Document main_test.go refactoring analysis (bd-1rh)
Analysis shows main_test.go is NOT a good candidate for shared DB pattern
due to global state manipulation and integration test characteristics.

Changes:
- Added MAIN_TEST_REFACTOR_NOTES.md documenting findings
- Fixed unused import in duplicates_test.go (from recent pull)

Key findings:
- 18 tests with 14 newTestStore() calls
- Tests manipulate global state (autoFlushEnabled, isDirty, etc.)
- Tests simulate workflows (flush, import) not just CRUD
- Shared DB causes deadlocks between flush ops and cleanup
- Integration tests need process-level isolation

Recommendation: Leave as-is or use Option 2 (grouped tests without
shared DB). Focus P2 efforts on integrity_test.go instead.

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 18:00:23 -05:00

265 lines
6.5 KiB
Go

package main
import (
"context"
"testing"
"github.com/steveyegge/beads/internal/types"
)
func TestFindDuplicateGroups(t *testing.T) {
tests := []struct {
name string
issues []*types.Issue
expectedGroups int
}{
{
name: "no duplicates",
issues: []*types.Issue{
{ID: "bd-1", Title: "Task 1", Status: types.StatusOpen},
{ID: "bd-2", Title: "Task 2", Status: types.StatusOpen},
},
expectedGroups: 0,
},
{
name: "simple duplicate",
issues: []*types.Issue{
{ID: "bd-1", Title: "Task 1", Status: types.StatusOpen},
{ID: "bd-2", Title: "Task 1", Status: types.StatusOpen},
},
expectedGroups: 1,
},
{
name: "duplicate with different status ignored",
issues: []*types.Issue{
{ID: "bd-1", Title: "Task 1", Status: types.StatusOpen},
{ID: "bd-2", Title: "Task 1", Status: types.StatusClosed},
},
expectedGroups: 0,
},
{
name: "multiple duplicates",
issues: []*types.Issue{
{ID: "bd-1", Title: "Task 1", Status: types.StatusOpen},
{ID: "bd-2", Title: "Task 1", Status: types.StatusOpen},
{ID: "bd-3", Title: "Task 2", Status: types.StatusOpen},
{ID: "bd-4", Title: "Task 2", Status: types.StatusOpen},
},
expectedGroups: 2,
},
{
name: "different descriptions are duplicates if title matches",
issues: []*types.Issue{
{ID: "bd-1", Title: "Task 1", Description: "Desc 1", Status: types.StatusOpen},
{ID: "bd-2", Title: "Task 1", Description: "Desc 2", Status: types.StatusOpen},
},
expectedGroups: 0, // Different descriptions = not duplicates
},
{
name: "exact content match",
issues: []*types.Issue{
{ID: "bd-1", Title: "Task 1", Description: "Desc 1", Design: "Design 1", AcceptanceCriteria: "AC 1", Status: types.StatusOpen},
{ID: "bd-2", Title: "Task 1", Description: "Desc 1", Design: "Design 1", AcceptanceCriteria: "AC 1", Status: types.StatusOpen},
},
expectedGroups: 1,
},
{
name: "three-way duplicate",
issues: []*types.Issue{
{ID: "bd-1", Title: "Task 1", Status: types.StatusOpen},
{ID: "bd-2", Title: "Task 1", Status: types.StatusOpen},
{ID: "bd-3", Title: "Task 1", Status: types.StatusOpen},
},
expectedGroups: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
groups := findDuplicateGroups(tt.issues)
if len(groups) != tt.expectedGroups {
t.Errorf("findDuplicateGroups() returned %d groups, want %d", len(groups), tt.expectedGroups)
}
})
}
}
func TestChooseMergeTarget(t *testing.T) {
tests := []struct {
name string
group []*types.Issue
refCounts map[string]int
wantID string
}{
{
name: "choose by reference count",
group: []*types.Issue{
{ID: "bd-2", Title: "Task"},
{ID: "bd-1", Title: "Task"},
},
refCounts: map[string]int{
"bd-1": 5,
"bd-2": 0,
},
wantID: "bd-1",
},
{
name: "choose by lexicographic order if same references",
group: []*types.Issue{
{ID: "bd-2", Title: "Task"},
{ID: "bd-1", Title: "Task"},
},
refCounts: map[string]int{
"bd-1": 0,
"bd-2": 0,
},
wantID: "bd-1",
},
{
name: "prefer higher references even with larger ID",
group: []*types.Issue{
{ID: "bd-1", Title: "Task"},
{ID: "bd-100", Title: "Task"},
},
refCounts: map[string]int{
"bd-1": 1,
"bd-100": 10,
},
wantID: "bd-100",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
target := chooseMergeTarget(tt.group, tt.refCounts)
if target.ID != tt.wantID {
t.Errorf("chooseMergeTarget() = %v, want %v", target.ID, tt.wantID)
}
})
}
}
func TestCountReferences(t *testing.T) {
issues := []*types.Issue{
{
ID: "bd-1",
Description: "See bd-2 for details",
Notes: "Related to bd-3",
},
{
ID: "bd-2",
Description: "Mentioned bd-1 twice: bd-1",
},
{
ID: "bd-3",
Notes: "Nothing to see here",
},
}
counts := countReferences(issues)
expectedCounts := map[string]int{
"bd-1": 2, // Referenced twice in bd-2
"bd-2": 1, // Referenced once in bd-1
"bd-3": 1, // Referenced once in bd-1
}
for id, expectedCount := range expectedCounts {
if counts[id] != expectedCount {
t.Errorf("countReferences()[%s] = %d, want %d", id, counts[id], expectedCount)
}
}
}
func TestDuplicateGroupsWithDifferentStatuses(t *testing.T) {
issues := []*types.Issue{
{ID: "bd-1", Title: "Task 1", Status: types.StatusOpen},
{ID: "bd-2", Title: "Task 1", Status: types.StatusClosed},
{ID: "bd-3", Title: "Task 1", Status: types.StatusOpen},
}
groups := findDuplicateGroups(issues)
// Should have 1 group with bd-1 and bd-3 (both open)
if len(groups) != 1 {
t.Fatalf("Expected 1 group, got %d", len(groups))
}
if len(groups[0]) != 2 {
t.Fatalf("Expected 2 issues in group, got %d", len(groups[0]))
}
// Verify bd-2 (closed) is not in the group
for _, issue := range groups[0] {
if issue.ID == "bd-2" {
t.Errorf("bd-2 (closed) should not be in group with open issues")
}
}
}
func TestDuplicatesIntegration(t *testing.T) {
tmpDir := t.TempDir()
testStore := newTestStore(t, tmpDir+"/.beads/beads.db")
ctx := context.Background()
// Create duplicate issues (let DB assign IDs)
issues := []*types.Issue{
{
Title: "Fix authentication bug",
Description: "Users can't login",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeBug,
},
{
Title: "Fix authentication bug",
Description: "Users can't login",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeBug,
},
{
Title: "Different task",
Description: "Different description",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
},
}
for _, issue := range issues {
if err := testStore.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
}
// Fetch all issues
allIssues, err := testStore.SearchIssues(ctx, "", types.IssueFilter{})
if err != nil {
t.Fatalf("SearchIssues failed: %v", err)
}
// Find duplicates
groups := findDuplicateGroups(allIssues)
if len(groups) != 1 {
t.Fatalf("Expected 1 duplicate group, got %d", len(groups))
}
if len(groups[0]) != 2 {
t.Fatalf("Expected 2 issues in group, got %d", len(groups[0]))
}
// Verify the duplicate group contains the two issues with "Fix authentication bug"
dupCount := 0
for _, issue := range groups[0] {
if issue.Title == "Fix authentication bug" {
dupCount++
}
}
if dupCount != 2 {
t.Errorf("Expected duplicate group to contain 2 'Fix authentication bug' issues, got %d", dupCount)
}
}