fix(sync): address code review issues in manual conflict resolution

Fixes from code review:
- Fix duplicate check in merge logic (use else clause)
- Handle io.EOF gracefully (treat as quit)
- Add quit (q) option to abort resolution early
- Add accept-all (a) option to auto-merge remaining conflicts
- Fix skipped conflicts to keep local version (not auto-merge)
- Handle json.MarshalIndent errors properly
- Fix truncateText to use rune count for UTF-8 safety
- Update help text with new options
- Add UTF-8 and emoji test cases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jane
2026-01-19 11:44:09 -08:00
committed by Steve Yegge
parent 5d68e6b61a
commit 80cd1f35c0
3 changed files with 202 additions and 39 deletions

View File

@@ -45,6 +45,36 @@ func TestTruncateText(t *testing.T) {
maxLen: 30,
want: "line1 line2 line3",
},
{
name: "very short max",
input: "hello world",
maxLen: 3,
want: "...",
},
{
name: "UTF-8 characters preserved",
input: "Hello 世界This is a test",
maxLen: 12,
want: "Hello 世界!...",
},
{
name: "UTF-8 exact length",
input: "日本語テスト",
maxLen: 6,
want: "日本語テスト",
},
{
name: "UTF-8 truncate",
input: "日本語テストです",
maxLen: 6,
want: "日本語...",
},
{
name: "emoji handling",
input: "Hello 🌍🌎🌏 World",
maxLen: 12,
want: "Hello 🌍🌎🌏...",
},
}
for _, tt := range tests {
@@ -164,6 +194,14 @@ func TestInteractiveConflictDisplay(t *testing.T) {
},
},
},
{
name: "both nil (edge case)",
conflict: InteractiveConflict{
IssueID: "test-6",
Local: nil,
Remote: nil,
},
},
}
for _, tt := range tests {
@@ -177,22 +215,58 @@ func TestInteractiveConflictDisplay(t *testing.T) {
func TestShowDetailedDiff(t *testing.T) {
now := time.Now()
conflict := InteractiveConflict{
IssueID: "test-1",
Local: &beads.Issue{
ID: "test-1",
Title: "Local",
UpdatedAt: now,
tests := []struct {
name string
conflict InteractiveConflict
}{
{
name: "both exist",
conflict: InteractiveConflict{
IssueID: "test-1",
Local: &beads.Issue{
ID: "test-1",
Title: "Local",
UpdatedAt: now,
},
Remote: &beads.Issue{
ID: "test-1",
Title: "Remote",
UpdatedAt: now,
},
},
},
Remote: &beads.Issue{
ID: "test-1",
Title: "Remote",
UpdatedAt: now,
{
name: "local nil",
conflict: InteractiveConflict{
IssueID: "test-2",
Local: nil,
Remote: &beads.Issue{
ID: "test-2",
Title: "Remote",
UpdatedAt: now,
},
},
},
{
name: "remote nil",
conflict: InteractiveConflict{
IssueID: "test-3",
Local: &beads.Issue{
ID: "test-3",
Title: "Local",
UpdatedAt: now,
},
Remote: nil,
},
},
}
// Just make sure it doesn't panic
showDetailedDiff(conflict)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Just make sure it doesn't panic
showDetailedDiff(tt.conflict)
})
}
}
func TestPrintResolutionHelp(t *testing.T) {
@@ -272,3 +346,34 @@ func TestInteractiveResolutionMerge(t *testing.T) {
t.Errorf("Expected labels to contain 'bug' and 'feature', got %v", merged.Labels)
}
}
func TestInteractiveResolutionChoices(t *testing.T) {
// Test InteractiveResolution struct values
tests := []struct {
name string
choice string
issue *beads.Issue
}{
{"local", "local", &beads.Issue{ID: "test"}},
{"remote", "remote", &beads.Issue{ID: "test"}},
{"merged", "merged", &beads.Issue{ID: "test"}},
{"skip", "skip", nil},
{"quit", "quit", nil},
{"accept-all", "accept-all", nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res := InteractiveResolution{Choice: tt.choice, Issue: tt.issue}
if res.Choice != tt.choice {
t.Errorf("Expected choice %q, got %q", tt.choice, res.Choice)
}
if tt.issue == nil && res.Issue != nil {
t.Errorf("Expected nil issue, got %v", res.Issue)
}
if tt.issue != nil && res.Issue == nil {
t.Errorf("Expected non-nil issue, got nil")
}
})
}
}