Implement auto-merge functionality for duplicates command

When --auto-merge is used, performMerge now automatically:
1. Closes source issues with "Duplicate of <target>" reason
2. Links each source to target with a "related" dependency

Closes bd-hdt

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-27 22:39:01 -08:00
parent f06f282d26
commit 1675275e1c
3 changed files with 218 additions and 67 deletions

View File

@@ -262,3 +262,107 @@ func TestDuplicatesIntegration(t *testing.T) {
t.Errorf("Expected duplicate group to contain 2 'Fix authentication bug' issues, got %d", dupCount)
}
}
func TestPerformMerge(t *testing.T) {
tmpDir := t.TempDir()
testStore := newTestStore(t, tmpDir+"/.beads/beads.db")
ctx := context.Background()
// Set up global state needed by performMerge
oldStore := store
oldRootCtx := rootCtx
oldActor := actor
store = testStore
rootCtx = ctx
actor = "test-user"
defer func() {
store = oldStore
rootCtx = oldRootCtx
actor = oldActor
}()
// Create duplicate issues
target := &types.Issue{
Title: "Main issue",
Description: "This is the target",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
source1 := &types.Issue{
Title: "Main issue",
Description: "This is the target",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
source2 := &types.Issue{
Title: "Main issue",
Description: "This is the target",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
for _, issue := range []*types.Issue{target, source1, source2} {
if err := testStore.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
}
// Perform the merge
result := performMerge(target.ID, []string{source1.ID, source2.ID})
// Verify result structure
closedIDs := result["closed"].([]string)
linkedIDs := result["linked"].([]string)
errors := result["errors"].([]string)
if len(closedIDs) != 2 {
t.Errorf("Expected 2 closed issues, got %d", len(closedIDs))
}
if len(linkedIDs) != 2 {
t.Errorf("Expected 2 linked issues, got %d", len(linkedIDs))
}
if len(errors) != 0 {
t.Errorf("Expected 0 errors, got %d: %v", len(errors), errors)
}
// Verify source issues are closed
for _, sourceID := range []string{source1.ID, source2.ID} {
issue, err := testStore.GetIssue(ctx, sourceID)
if err != nil {
t.Fatalf("GetIssue(%s) failed: %v", sourceID, err)
}
if issue.Status != types.StatusClosed {
t.Errorf("Issue %s should be closed, got status %s", sourceID, issue.Status)
}
}
// Verify target is still open
targetIssue, err := testStore.GetIssue(ctx, target.ID)
if err != nil {
t.Fatalf("GetIssue(%s) failed: %v", target.ID, err)
}
if targetIssue.Status != types.StatusOpen {
t.Errorf("Target issue should still be open, got status %s", targetIssue.Status)
}
// Verify dependencies were created (GetDependencies returns issues this depends on)
for _, sourceID := range []string{source1.ID, source2.ID} {
deps, err := testStore.GetDependencies(ctx, sourceID)
if err != nil {
t.Fatalf("GetDependencies(%s) failed: %v", sourceID, err)
}
found := false
for _, dep := range deps {
if dep.ID == target.ID {
found = true
break
}
}
if !found {
t.Errorf("Expected dependency from %s to %s", sourceID, target.ID)
}
}
}