- Replace timestamp-based collision scoring with deterministic content hashing - Add hashIssueContent() using SHA-256 of all substantive fields - Modify ScoreCollisions to compare hashes and set RemapIncoming flag - Update RemapCollisions to handle both directions (remap incoming OR existing) - Add CollisionDetail.RemapIncoming field to control which version gets remapped - Add unit tests for hash function and deterministic collision resolution Status: Hash-based resolution works correctly, but TestTwoCloneCollision still fails due to missing rename detection. After Clone B resolves collision, Clone A needs to recognize its issue was remapped to a different ID. Next: Add content-based rename detection during import to prevent re-resolving already-resolved collisions. Progress on bd-86. Amp-Thread-ID: https://ampcode.com/threads/T-b19b49e8-b52a-463d-b052-8a526a500260 Co-authored-by: Amp <amp@ampcode.com>
91 lines
2.2 KiB
Go
91 lines
2.2 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/steveyegge/beads/internal/types"
|
|
)
|
|
|
|
func TestHashIssueContent(t *testing.T) {
|
|
issue1 := &types.Issue{
|
|
Title: "Issue from clone A",
|
|
Description: "",
|
|
Priority: 1,
|
|
IssueType: "task",
|
|
Status: "open",
|
|
}
|
|
|
|
issue2 := &types.Issue{
|
|
Title: "Issue from clone B",
|
|
Description: "",
|
|
Priority: 1,
|
|
IssueType: "task",
|
|
Status: "open",
|
|
}
|
|
|
|
hash1 := hashIssueContent(issue1)
|
|
hash2 := hashIssueContent(issue2)
|
|
|
|
// Hashes should be different
|
|
if hash1 == hash2 {
|
|
t.Errorf("Expected different hashes, got same: %s", hash1)
|
|
}
|
|
|
|
// Hashes should be deterministic
|
|
hash1Again := hashIssueContent(issue1)
|
|
if hash1 != hash1Again {
|
|
t.Errorf("Hash not deterministic: %s != %s", hash1, hash1Again)
|
|
}
|
|
|
|
t.Logf("Hash A: %s", hash1)
|
|
t.Logf("Hash B: %s", hash2)
|
|
t.Logf("A < B: %v (B wins if true)", hash1 < hash2)
|
|
}
|
|
|
|
func TestScoreCollisions_Deterministic(t *testing.T) {
|
|
existingIssue := &types.Issue{
|
|
ID: "test-1",
|
|
Title: "Issue from clone B",
|
|
Description: "",
|
|
Priority: 1,
|
|
IssueType: "task",
|
|
Status: "open",
|
|
}
|
|
|
|
incomingIssue := &types.Issue{
|
|
ID: "test-1",
|
|
Title: "Issue from clone A",
|
|
Description: "",
|
|
Priority: 1,
|
|
IssueType: "task",
|
|
Status: "open",
|
|
}
|
|
|
|
collision := &CollisionDetail{
|
|
ID: "test-1",
|
|
ExistingIssue: existingIssue,
|
|
IncomingIssue: incomingIssue,
|
|
}
|
|
|
|
// Run scoring
|
|
err := ScoreCollisions(nil, nil, []*CollisionDetail{collision}, nil)
|
|
if err != nil {
|
|
t.Fatalf("ScoreCollisions failed: %v", err)
|
|
}
|
|
|
|
existingHash := hashIssueContent(existingIssue)
|
|
incomingHash := hashIssueContent(incomingIssue)
|
|
|
|
t.Logf("Existing hash (B): %s", existingHash)
|
|
t.Logf("Incoming hash (A): %s", incomingHash)
|
|
t.Logf("Existing < Incoming: %v", existingHash < incomingHash)
|
|
|
|
// Clone B has lower hash, so it should win
|
|
// This means: RemapIncoming should be TRUE (remap incoming A, keep existing B)
|
|
if !collision.RemapIncoming {
|
|
t.Errorf("Expected RemapIncoming=true (remap incoming A, keep existing B with lower hash), got false")
|
|
} else {
|
|
t.Logf("✓ Correct: RemapIncoming=true, will remap incoming 'clone A' and keep existing 'clone B'")
|
|
}
|
|
}
|