feat(sync): add safety check enhancements and merge fixes
- Add forensic logging for mass deletions (bd-lsa): log vanished issue IDs and titles - Add sync.require_confirmation_on_mass_delete config option (bd-4u8) - Fix priority merge to treat 0 as "unset" (bd-d0t) - Fix timestamp tie-breaker to prefer left/local (bd-8nz) - Add warning log when extraction fails during safety check (bd-feh) - Refactor safety warnings to return in PullResult (bd-7z4) - Add TestSafetyCheckMassDeletion integration tests (bd-cnn) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -426,6 +426,8 @@ func mergeNotes(base, left, right string) string {
|
||||
}
|
||||
|
||||
// mergePriority handles priority merging - on conflict, higher priority wins (lower number)
|
||||
// Special case: 0 is treated as "unset/no priority" due to Go's zero value.
|
||||
// Any explicitly set priority (>0) wins over 0. (bd-d0t fix)
|
||||
func mergePriority(base, left, right int) int {
|
||||
// Standard 3-way merge for non-conflict cases
|
||||
if base == left && base != right {
|
||||
@@ -438,7 +440,16 @@ func mergePriority(base, left, right int) int {
|
||||
return left
|
||||
}
|
||||
// True conflict: both sides changed to different values
|
||||
// Higher priority wins (lower number = more urgent)
|
||||
|
||||
// bd-d0t fix: Treat 0 as "unset" - explicitly set priority wins over unset
|
||||
if left == 0 && right > 0 {
|
||||
return right // right has explicit priority, left is unset
|
||||
}
|
||||
if right == 0 && left > 0 {
|
||||
return left // left has explicit priority, right is unset
|
||||
}
|
||||
|
||||
// Both have explicit priorities (or both are 0) - higher priority wins (lower number = more urgent)
|
||||
if left < right {
|
||||
return left
|
||||
}
|
||||
@@ -477,9 +488,9 @@ func isTimeAfter(t1, t2 string) bool {
|
||||
return true // t1 valid, t2 invalid - t1 wins
|
||||
}
|
||||
|
||||
// Both valid - compare. On exact tie, return false (right wins for now)
|
||||
// TODO: Consider preferring left on tie for consistency with IssueType rule
|
||||
return time1.After(time2)
|
||||
// Both valid - compare. On exact tie, left wins for consistency with IssueType rule (bd-8nz)
|
||||
// Using !time2.After(time1) returns true when t1 > t2 OR t1 == t2
|
||||
return !time2.After(time1)
|
||||
}
|
||||
|
||||
func maxTime(t1, t2 string) string {
|
||||
|
||||
@@ -337,10 +337,10 @@ func TestIsTimeAfter(t *testing.T) {
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "identical timestamps - right wins (false)",
|
||||
name: "identical timestamps - left wins (bd-8nz)",
|
||||
t1: "2024-01-01T00:00:00Z",
|
||||
t2: "2024-01-01T00:00:00Z",
|
||||
expected: false,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "t1 invalid, t2 valid - t2 wins",
|
||||
@@ -483,6 +483,99 @@ func TestMerge3Way_SimpleUpdates(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestMergePriority tests priority merging including bd-d0t fix
|
||||
func TestMergePriority(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
base int
|
||||
left int
|
||||
right int
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
name: "no changes",
|
||||
base: 2,
|
||||
left: 2,
|
||||
right: 2,
|
||||
expected: 2,
|
||||
},
|
||||
{
|
||||
name: "left changed",
|
||||
base: 2,
|
||||
left: 1,
|
||||
right: 2,
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "right changed",
|
||||
base: 2,
|
||||
left: 2,
|
||||
right: 3,
|
||||
expected: 3,
|
||||
},
|
||||
{
|
||||
name: "both changed to same value",
|
||||
base: 2,
|
||||
left: 1,
|
||||
right: 1,
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "conflict - higher priority wins (lower number)",
|
||||
base: 2,
|
||||
left: 3,
|
||||
right: 1,
|
||||
expected: 1,
|
||||
},
|
||||
// bd-d0t fix: 0 is treated as "unset"
|
||||
{
|
||||
name: "bd-d0t: left unset (0), right has explicit priority",
|
||||
base: 2,
|
||||
left: 0,
|
||||
right: 3,
|
||||
expected: 3, // explicit priority wins over unset
|
||||
},
|
||||
{
|
||||
name: "bd-d0t: left has explicit priority, right unset (0)",
|
||||
base: 2,
|
||||
left: 3,
|
||||
right: 0,
|
||||
expected: 3, // explicit priority wins over unset
|
||||
},
|
||||
{
|
||||
name: "bd-d0t: both unset (0)",
|
||||
base: 2,
|
||||
left: 0,
|
||||
right: 0,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "bd-d0t: base unset, left sets priority, right unchanged",
|
||||
base: 0,
|
||||
left: 1,
|
||||
right: 0,
|
||||
expected: 1, // left changed from 0 to 1
|
||||
},
|
||||
{
|
||||
name: "bd-d0t: base unset, right sets priority, left unchanged",
|
||||
base: 0,
|
||||
left: 0,
|
||||
right: 2,
|
||||
expected: 2, // right changed from 0 to 2
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := mergePriority(tt.base, tt.left, tt.right)
|
||||
if result != tt.expected {
|
||||
t.Errorf("mergePriority(%d, %d, %d) = %d, want %d",
|
||||
tt.base, tt.left, tt.right, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMerge3Way_AutoResolve tests auto-resolution of conflicts
|
||||
func TestMerge3Way_AutoResolve(t *testing.T) {
|
||||
t.Run("conflicting title changes - latest updated_at wins", func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user