Files
beads/internal/storage/sqlite/collision_hash_test.go
Steve Yegge 2e87329cf8 WIP: Implement content-hash based collision resolution (bd-89)
- 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>
2025-10-28 17:11:40 -07:00

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'")
}
}