Make DetectCollisions read-only (bd-96)
- Add RenameDetail type to track content matches with different IDs - Remove deletion logic from DetectCollisions (now read-only) - Create ApplyCollisionResolution to handle all modifications - Update importer.go to use two-phase approach (detect then apply) - Fix dependency preservation in RemapCollisions - Collect all dependencies before CASCADE DELETE - Recreate with updated IDs after remapping - Add tests: TestDetectCollisionsReadOnly, TestApplyCollisionResolution - Update collision tests for content-hash scoring behavior - Create bd-100 to track fixing autoimport tests
This commit is contained in:
@@ -141,45 +141,52 @@ func TestRemapCollisionsRemapsImportedNotExisting(t *testing.T) {
|
||||
t.Fatalf("RemapCollisions failed: %v", err)
|
||||
}
|
||||
|
||||
// Step 5: Verify existing issue dependencies are preserved
|
||||
t.Logf("\n=== Verifying Existing Issue Dependencies ===")
|
||||
// Step 5: Verify dependencies are preserved on remapped issues
|
||||
// With content-hash scoring, all existing issues get remapped to new IDs
|
||||
t.Logf("\n=== Verifying Dependencies Preserved on Remapped Issues ===")
|
||||
t.Logf("ID Mappings: %v", idMapping)
|
||||
|
||||
// Check bd-1 → bd-2 dependency (created before import)
|
||||
existingDeps1, _ := store.GetDependencyRecords(ctx, "bd-1")
|
||||
t.Logf("bd-1 dependencies: %d (expected: 1)", len(existingDeps1))
|
||||
|
||||
if len(existingDeps1) == 0 {
|
||||
t.Errorf("BUG CONFIRMED: Existing bd-1 has ZERO dependencies after import!")
|
||||
t.Errorf(" Expected: bd-1 → bd-2 (created by test before import)")
|
||||
t.Errorf(" Actual: All dependencies deleted by updateDependencyReferences()")
|
||||
} else if len(existingDeps1) != 1 {
|
||||
t.Errorf("Expected 1 dependency for bd-1, got %d", len(existingDeps1))
|
||||
} else {
|
||||
// Verify the dependency is correct
|
||||
if existingDeps1[0].DependsOnID != "bd-2" {
|
||||
t.Errorf("Expected bd-1 → bd-2, got bd-1 → %s", existingDeps1[0].DependsOnID)
|
||||
}
|
||||
// The new bd-1, bd-2, bd-3 (incoming issues) should have NO dependencies
|
||||
newBD1Deps, _ := store.GetDependencyRecords(ctx, "bd-1")
|
||||
if len(newBD1Deps) != 0 {
|
||||
t.Errorf("Expected 0 dependencies for new bd-1 (incoming), got %d", len(newBD1Deps))
|
||||
}
|
||||
|
||||
// Check bd-3 → bd-1 dependency (created before import)
|
||||
existingDeps3, _ := store.GetDependencyRecords(ctx, "bd-3")
|
||||
t.Logf("bd-3 dependencies: %d (expected: 1)", len(existingDeps3))
|
||||
|
||||
if len(existingDeps3) == 0 {
|
||||
t.Errorf("BUG CONFIRMED: Existing bd-3 has ZERO dependencies after import!")
|
||||
t.Errorf(" Expected: bd-3 → bd-1 (created by test before import)")
|
||||
t.Errorf(" Actual: All dependencies deleted by updateDependencyReferences()")
|
||||
} else if len(existingDeps3) != 1 {
|
||||
t.Errorf("Expected 1 dependency for bd-3, got %d", len(existingDeps3))
|
||||
} else {
|
||||
// Verify the dependency is correct
|
||||
if existingDeps3[0].DependsOnID != testIssueBD1 {
|
||||
t.Errorf("Expected bd-3 → bd-1, got bd-3 → %s", existingDeps3[0].DependsOnID)
|
||||
}
|
||||
newBD3Deps, _ := store.GetDependencyRecords(ctx, "bd-3")
|
||||
if len(newBD3Deps) != 0 {
|
||||
t.Errorf("Expected 0 dependencies for new bd-3 (incoming), got %d", len(newBD3Deps))
|
||||
}
|
||||
|
||||
t.Logf("\nID Mappings: %v", idMapping)
|
||||
t.Logf("Fix verified: Existing issue dependencies preserved during collision resolution")
|
||||
// The remapped issues should have their dependencies preserved
|
||||
remappedBD1 := idMapping["bd-1"] // Old bd-1 → new ID
|
||||
remappedBD2 := idMapping["bd-2"] // Old bd-2 → new ID
|
||||
remappedBD3 := idMapping["bd-3"] // Old bd-3 → new ID
|
||||
|
||||
// Check remapped bd-1's dependency (was bd-1 → bd-2, now should be remappedBD1 → remappedBD2)
|
||||
remappedBD1Deps, _ := store.GetDependencyRecords(ctx, remappedBD1)
|
||||
t.Logf("%s dependencies: %d (expected: 1)", remappedBD1, len(remappedBD1Deps))
|
||||
|
||||
if len(remappedBD1Deps) != 1 {
|
||||
t.Errorf("Expected 1 dependency for remapped %s (preserved from old bd-1), got %d",
|
||||
remappedBD1, len(remappedBD1Deps))
|
||||
} else if remappedBD1Deps[0].DependsOnID != remappedBD2 {
|
||||
t.Errorf("Expected %s → %s, got %s → %s",
|
||||
remappedBD1, remappedBD2, remappedBD1, remappedBD1Deps[0].DependsOnID)
|
||||
}
|
||||
|
||||
// Check remapped bd-3's dependency (was bd-3 → bd-1, now should be remappedBD3 → remappedBD1)
|
||||
remappedBD3Deps, _ := store.GetDependencyRecords(ctx, remappedBD3)
|
||||
t.Logf("%s dependencies: %d (expected: 1)", remappedBD3, len(remappedBD3Deps))
|
||||
|
||||
if len(remappedBD3Deps) != 1 {
|
||||
t.Errorf("Expected 1 dependency for remapped %s (preserved from old bd-3), got %d",
|
||||
remappedBD3, len(remappedBD3Deps))
|
||||
} else if remappedBD3Deps[0].DependsOnID != remappedBD1 {
|
||||
t.Errorf("Expected %s → %s, got %s → %s",
|
||||
remappedBD3, remappedBD1, remappedBD3, remappedBD3Deps[0].DependsOnID)
|
||||
}
|
||||
|
||||
t.Logf("Fix verified: Dependencies preserved correctly on remapped issues with content-hash scoring")
|
||||
}
|
||||
|
||||
// TestRemapCollisionsDoesNotUpdateNonexistentDependencies verifies that
|
||||
@@ -264,32 +271,37 @@ func TestRemapCollisionsDoesNotUpdateNonexistentDependencies(t *testing.T) {
|
||||
t.Fatalf("RemapCollisions failed: %v", err)
|
||||
}
|
||||
|
||||
// Step 3: Verify existing dependency is untouched
|
||||
existingDeps, err := store.GetDependencyRecords(ctx, "bd-1")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get dependencies for bd-1: %v", err)
|
||||
}
|
||||
|
||||
if len(existingDeps) != 1 {
|
||||
t.Errorf("Expected 1 dependency for existing bd-1, got %d (dependency should not be touched)", len(existingDeps))
|
||||
} else {
|
||||
if existingDeps[0].DependsOnID != testIssueBD2 {
|
||||
t.Errorf("Expected bd-1 → bd-2, got bd-1 → %s", existingDeps[0].DependsOnID)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the remapped issue exists but has no dependencies
|
||||
// (because dependencies are imported later in Phase 5)
|
||||
// Step 3: Verify dependencies are preserved correctly
|
||||
// With content-hash scoring: existing hash > incoming hash, so RemapIncoming=false
|
||||
// This means: existing bd-1 → remapped to new ID, incoming bd-1 takes over bd-1
|
||||
|
||||
// The remapped issue (old bd-1) should have its dependency preserved
|
||||
remappedID := idMapping["bd-1"]
|
||||
remappedDeps, err := store.GetDependencyRecords(ctx, remappedID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get dependencies for %s: %v", remappedID, err)
|
||||
}
|
||||
|
||||
if len(remappedDeps) != 0 {
|
||||
t.Errorf("Expected 0 dependencies for remapped %s (dependencies added later), got %d",
|
||||
if len(remappedDeps) != 1 {
|
||||
t.Errorf("Expected 1 dependency for remapped %s (preserved from old bd-1), got %d",
|
||||
remappedID, len(remappedDeps))
|
||||
} else {
|
||||
// The dependency should now be remappedID → bd-2 (updated from bd-1 → bd-2)
|
||||
if remappedDeps[0].DependsOnID != testIssueBD2 {
|
||||
t.Errorf("Expected %s → bd-2, got %s → %s", remappedID, remappedID, remappedDeps[0].DependsOnID)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Verified: updateDependencyReferences is effectively a no-op when no remapped dependencies exist")
|
||||
// The new bd-1 (incoming issue) should have no dependencies
|
||||
// (because dependencies are imported later in Phase 5)
|
||||
newBD1Deps, err := store.GetDependencyRecords(ctx, "bd-1")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get dependencies for bd-1: %v", err)
|
||||
}
|
||||
|
||||
if len(newBD1Deps) != 0 {
|
||||
t.Errorf("Expected 0 dependencies for new bd-1 (dependencies added later), got %d", len(newBD1Deps))
|
||||
}
|
||||
|
||||
t.Logf("Verified: Dependencies preserved correctly during collision resolution with content-hash scoring")
|
||||
}
|
||||
|
||||
@@ -189,6 +189,7 @@ func handlePrefixMismatch(ctx context.Context, sqliteStore *sqlite.SQLiteStorage
|
||||
|
||||
// handleCollisions detects and resolves ID collisions
|
||||
func handleCollisions(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issues []*types.Issue, opts Options, result *Result) ([]*types.Issue, error) {
|
||||
// Phase 1: Detect (read-only)
|
||||
collisionResult, err := sqlite.DetectCollisions(ctx, sqliteStore, issues)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("collision detection failed: %w", err)
|
||||
@@ -215,12 +216,12 @@ func handleCollisions(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, is
|
||||
return nil, fmt.Errorf("failed to get existing issues for collision resolution: %w", err)
|
||||
}
|
||||
|
||||
// Score collisions
|
||||
// Phase 2: Score collisions
|
||||
if err := sqlite.ScoreCollisions(ctx, sqliteStore, collisionResult.Collisions, allExistingIssues); err != nil {
|
||||
return nil, fmt.Errorf("failed to score collisions: %w", err)
|
||||
}
|
||||
|
||||
// Remap collisions
|
||||
// Phase 3: Remap collisions
|
||||
idMapping, err := sqlite.RemapCollisions(ctx, sqliteStore, collisionResult.Collisions, allExistingIssues)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to remap collisions: %w", err)
|
||||
@@ -243,8 +244,20 @@ func handleCollisions(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, is
|
||||
return filteredIssues, nil
|
||||
}
|
||||
|
||||
// Phase 4: Apply renames (deletions of old IDs) if any were detected
|
||||
if len(collisionResult.Renames) > 0 && !opts.DryRun {
|
||||
// Build mapping for renames: oldID -> newID
|
||||
renameMapping := make(map[string]string)
|
||||
for _, rename := range collisionResult.Renames {
|
||||
renameMapping[rename.OldID] = rename.NewID
|
||||
}
|
||||
if err := sqlite.ApplyCollisionResolution(ctx, sqliteStore, collisionResult, renameMapping); err != nil {
|
||||
return nil, fmt.Errorf("failed to apply rename resolutions: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if opts.DryRun {
|
||||
result.Created = len(collisionResult.NewIssues)
|
||||
result.Created = len(collisionResult.NewIssues) + len(collisionResult.Renames)
|
||||
result.Unchanged = len(collisionResult.ExactMatches)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,14 @@ type CollisionResult struct {
|
||||
ExactMatches []string // IDs that match exactly (idempotent import)
|
||||
Collisions []*CollisionDetail // Issues with same ID but different content
|
||||
NewIssues []string // IDs that don't exist in DB yet
|
||||
Renames []*RenameDetail // Issues with same content but different ID (renames)
|
||||
}
|
||||
|
||||
// RenameDetail captures a rename/remap detected during collision detection
|
||||
type RenameDetail struct {
|
||||
OldID string // ID in database (to be deleted)
|
||||
NewID string // ID in incoming (to be created)
|
||||
Issue *types.Issue // The issue with new ID
|
||||
}
|
||||
|
||||
// CollisionDetail provides detailed information about a collision
|
||||
@@ -74,16 +82,13 @@ func DetectCollisions(ctx context.Context, s *SQLiteStorage, incomingIssues []*t
|
||||
if dbMatch, found := contentToDBIssue[incomingHash]; found {
|
||||
// Same content, different ID - this is a rename/remap
|
||||
// The incoming ID is the NEW canonical ID, existing DB ID is OLD
|
||||
// We should DELETE the old ID and ACCEPT the new one
|
||||
// Mark this as new issue (it will be created later)
|
||||
// and we'll handle deletion of old ID separately
|
||||
result.NewIssues = append(result.NewIssues, incoming.ID)
|
||||
|
||||
// Delete the old DB issue (content match with different ID)
|
||||
if err := s.DeleteIssue(ctx, dbMatch.ID); err != nil {
|
||||
return nil, fmt.Errorf("failed to delete renamed issue %s (renamed to %s): %w",
|
||||
dbMatch.ID, incoming.ID, err)
|
||||
}
|
||||
// Record this as a rename to be handled later (read-only detection)
|
||||
result.Renames = append(result.Renames, &RenameDetail{
|
||||
OldID: dbMatch.ID,
|
||||
NewID: incoming.ID,
|
||||
Issue: incoming,
|
||||
})
|
||||
// Don't add to NewIssues - will be handled by ApplyCollisionResolution
|
||||
} else {
|
||||
// Truly new issue
|
||||
result.NewIssues = append(result.NewIssues, incoming.ID)
|
||||
@@ -213,6 +218,32 @@ func hashIssueContent(issue *types.Issue) string {
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// ApplyCollisionResolution applies the modifications detected during collision detection.
|
||||
// This function handles:
|
||||
// 1. Rename deletions (delete old IDs for renamed issues)
|
||||
// 2. Creating remapped issues (based on mapping)
|
||||
// 3. Updating all references to use new IDs
|
||||
//
|
||||
// This is the write-phase counterpart to the read-only DetectCollisions.
|
||||
func ApplyCollisionResolution(ctx context.Context, s *SQLiteStorage, result *CollisionResult, mapping map[string]string) error {
|
||||
// Phase 1: Handle renames (delete old IDs)
|
||||
for _, rename := range result.Renames {
|
||||
if err := s.DeleteIssue(ctx, rename.OldID); err != nil {
|
||||
return fmt.Errorf("failed to delete renamed issue %s (renamed to %s): %w",
|
||||
rename.OldID, rename.NewID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Update references using the mapping
|
||||
if len(mapping) > 0 {
|
||||
if err := updateReferences(ctx, s, mapping); err != nil {
|
||||
return fmt.Errorf("failed to update references: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ScoreCollisions determines which version of each colliding issue to keep vs. remap.
|
||||
// Uses deterministic content-based hashing to ensure all clones make the same decision.
|
||||
//
|
||||
@@ -373,7 +404,14 @@ func RemapCollisions(ctx context.Context, s *SQLiteStorage, collisions []*Collis
|
||||
return nil, fmt.Errorf("failed to sync ID counters: %w", err)
|
||||
}
|
||||
|
||||
// Process each collision based on which version should be remapped
|
||||
// Step 1: Collect ALL dependencies before any modifications
|
||||
// This prevents CASCADE DELETE from losing dependency information
|
||||
allDepsBeforeRemap, err := s.GetAllDependencyRecords(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get all dependencies: %w", err)
|
||||
}
|
||||
|
||||
// Step 2: Process each collision based on which version should be remapped
|
||||
for _, collision := range collisions {
|
||||
// Skip collisions with nil issues (shouldn't happen but be defensive)
|
||||
if collision.IncomingIssue == nil {
|
||||
@@ -410,7 +448,7 @@ func RemapCollisions(ctx context.Context, s *SQLiteStorage, collisions []*Collis
|
||||
}
|
||||
} else {
|
||||
// Existing has higher hash -> remap existing, replace with incoming
|
||||
// First, remap the existing issue to new ID
|
||||
// Record mapping FIRST before any operations
|
||||
idMapping[oldID] = newID
|
||||
|
||||
// Create a copy of existing issue with new ID
|
||||
@@ -421,19 +459,70 @@ func RemapCollisions(ctx context.Context, s *SQLiteStorage, collisions []*Collis
|
||||
return nil, fmt.Errorf("failed to create remapped existing issue %s -> %s: %w", oldID, newID, err)
|
||||
}
|
||||
|
||||
// Delete the existing issue with old ID
|
||||
// Create incoming issue with original ID (this will REPLACE when we delete old ID)
|
||||
// We do this BEFORE deleting so both issues exist temporarily
|
||||
// Note: This will fail if incoming ID already exists, which is expected in this flow
|
||||
// So we skip this step and do it after deletion
|
||||
|
||||
// Note: We do NOT copy dependencies here - DeleteIssue will cascade delete them
|
||||
// But we've already recorded the mapping, so updateReferences will fix everything
|
||||
// after all collisions are processed
|
||||
|
||||
// Delete the existing issue with old ID (this will cascade delete old dependencies)
|
||||
if err := s.DeleteIssue(ctx, oldID); err != nil {
|
||||
return nil, fmt.Errorf("failed to delete old existing issue %s: %w", oldID, err)
|
||||
}
|
||||
|
||||
// Create incoming issue with original ID (replaces the deleted one)
|
||||
// NOW create incoming issue with original ID (replaces the deleted one)
|
||||
if err := s.CreateIssue(ctx, collision.IncomingIssue, "import-replace"); err != nil {
|
||||
return nil, fmt.Errorf("failed to create incoming issue %s: %w", oldID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now update all references in text fields and dependencies
|
||||
// Step 3: Recreate dependencies with updated IDs
|
||||
// For each dependency that involved a remapped issue, recreate it with new IDs
|
||||
for issueID, deps := range allDepsBeforeRemap {
|
||||
for _, dep := range deps {
|
||||
// Determine new IDs (use mapping if available, otherwise keep original)
|
||||
newIssueID := issueID
|
||||
if mappedID, ok := idMapping[issueID]; ok {
|
||||
newIssueID = mappedID
|
||||
}
|
||||
|
||||
newDependsOnID := dep.DependsOnID
|
||||
if mappedID, ok := idMapping[dep.DependsOnID]; ok {
|
||||
newDependsOnID = mappedID
|
||||
}
|
||||
|
||||
// Only recreate if at least one ID was remapped
|
||||
if newIssueID != issueID || newDependsOnID != dep.DependsOnID {
|
||||
// Check if both issues still exist (the source might have been replaced)
|
||||
sourceExists, err := s.GetIssue(ctx, newIssueID)
|
||||
if err != nil || sourceExists == nil {
|
||||
continue // Skip if source was deleted/replaced
|
||||
}
|
||||
|
||||
targetExists, err := s.GetIssue(ctx, newDependsOnID)
|
||||
if err != nil || targetExists == nil {
|
||||
continue // Skip if target doesn't exist
|
||||
}
|
||||
|
||||
// Create the dependency with new IDs
|
||||
newDep := &types.Dependency{
|
||||
IssueID: newIssueID,
|
||||
DependsOnID: newDependsOnID,
|
||||
Type: dep.Type,
|
||||
}
|
||||
if err := s.addDependencyUnchecked(ctx, newDep, "import-remap"); err != nil {
|
||||
// Ignore duplicate dependency errors
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Update all text field references
|
||||
if err := updateReferences(ctx, s, idMapping); err != nil {
|
||||
return nil, fmt.Errorf("failed to update references: %w", err)
|
||||
}
|
||||
|
||||
@@ -998,3 +998,146 @@ func BenchmarkReplaceIDReferencesMultipleTexts(b *testing.B) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestDetectCollisionsReadOnly verifies that DetectCollisions does not modify the database
|
||||
func TestDetectCollisionsReadOnly(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "collision-readonly-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
store, err := New(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create storage: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if err := store.SetConfig(ctx, "issue_prefix", "bd"); err != nil {
|
||||
t.Fatalf("failed to set issue_prefix: %v", err)
|
||||
}
|
||||
|
||||
// Create an issue in the database
|
||||
dbIssue := &types.Issue{
|
||||
ID: "bd-1",
|
||||
Title: "Original issue",
|
||||
Description: "Original content",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, dbIssue, "test"); err != nil {
|
||||
t.Fatalf("failed to create DB issue: %v", err)
|
||||
}
|
||||
|
||||
// Create incoming issue with SAME CONTENT but DIFFERENT ID (rename scenario)
|
||||
incomingIssue := &types.Issue{
|
||||
ID: "bd-100",
|
||||
Title: "Original issue",
|
||||
Description: "Original content",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
|
||||
// Call DetectCollisions
|
||||
result, err := DetectCollisions(ctx, store, []*types.Issue{incomingIssue})
|
||||
if err != nil {
|
||||
t.Fatalf("DetectCollisions failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify rename was detected
|
||||
if len(result.Renames) != 1 {
|
||||
t.Fatalf("expected 1 rename, got %d", len(result.Renames))
|
||||
}
|
||||
if result.Renames[0].OldID != "bd-1" {
|
||||
t.Errorf("expected OldID bd-1, got %s", result.Renames[0].OldID)
|
||||
}
|
||||
if result.Renames[0].NewID != "bd-100" {
|
||||
t.Errorf("expected NewID bd-100, got %s", result.Renames[0].NewID)
|
||||
}
|
||||
|
||||
// CRITICAL: Verify the old issue still exists in the database (not deleted)
|
||||
oldIssue, err := store.GetIssue(ctx, "bd-1")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get old issue: %v", err)
|
||||
}
|
||||
if oldIssue == nil {
|
||||
t.Fatal("old issue bd-1 was deleted - DetectCollisions is not read-only!")
|
||||
}
|
||||
if oldIssue.Title != "Original issue" {
|
||||
t.Errorf("old issue was modified - expected title 'Original issue', got '%s'", oldIssue.Title)
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyCollisionResolution verifies that ApplyCollisionResolution correctly applies renames
|
||||
func TestApplyCollisionResolution(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "apply-resolution-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
store, err := New(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create storage: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if err := store.SetConfig(ctx, "issue_prefix", "bd"); err != nil {
|
||||
t.Fatalf("failed to set issue_prefix: %v", err)
|
||||
}
|
||||
|
||||
// Create an issue to be renamed
|
||||
oldIssue := &types.Issue{
|
||||
ID: "bd-1",
|
||||
Title: "Issue to rename",
|
||||
Description: "Content",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, oldIssue, "test"); err != nil {
|
||||
t.Fatalf("failed to create old issue: %v", err)
|
||||
}
|
||||
|
||||
// Create a collision result with a rename
|
||||
newIssue := &types.Issue{
|
||||
ID: "bd-100",
|
||||
Title: "Issue to rename",
|
||||
Description: "Content",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
result := &CollisionResult{
|
||||
Renames: []*RenameDetail{
|
||||
{
|
||||
OldID: "bd-1",
|
||||
NewID: "bd-100",
|
||||
Issue: newIssue,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Apply the resolution
|
||||
emptyMapping := make(map[string]string)
|
||||
if err := ApplyCollisionResolution(ctx, store, result, emptyMapping); err != nil {
|
||||
t.Fatalf("ApplyCollisionResolution failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify old issue was deleted
|
||||
oldDeleted, err := store.GetIssue(ctx, "bd-1")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to check old issue: %v", err)
|
||||
}
|
||||
if oldDeleted != nil {
|
||||
t.Error("old issue bd-1 was not deleted")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user