fix(autoimport): auto-correct deleted status to tombstone for JSONL compatibility (#1231)

* fix(autoimport): auto-correct deleted status to tombstone for JSONL compatibility (GH#1223)

This fix addresses the 'Stuck in sync diversion loop' issue where v0.48.0
encountered validation errors during JSONL import. The issue occurs when
JSONL files from older versions have issues with status='deleted' but the
current code expects status='tombstone' for deleted issues.

Changes:
- Add migration logic in parseJSONL to auto-correct 'deleted' status to 'tombstone'
- Ensure tombstones always have deleted_at timestamp set
- Add debug logging for both migration operations
- Prevents users from being stuck in sync divergence when upgrading

Fixes GH#1223: Stuck in sync diversion loop

* fix(autoimport): comprehensively fix corrupted deleted_at on non-tombstone issues (GH#1223)

The initial fix for GH#1223 only caught issues with status='deleted', but the real
data in the wild had issues with status='closed' (or other statuses) but also
had deleted_at set, which violates the validation rule.

Changes:
- Add broader migration logic: any non-tombstone issue with deleted_at should become tombstone
- Apply fix in all three JSONL parsing locations:
  - internal/autoimport/autoimport.go (parseJSONL for auto-import)
  - cmd/bd/import.go (import command)
  - cmd/bd/daemon_sync.go (daemon sync helper)
- Add comprehensive test case for corrupted closed issues with deleted_at
- Fixes the 'non-tombstone issues cannot have deleted_at timestamp' validation error
  during fresh bd init or import

Fixes GH#1223: Stuck in sync diversion loop

* Add merge driver comment to .gitattributes

* fix: properly clean up .gitattributes during bd admin reset

Fixes GH#1223 - Stuck in sync diversion loop

The removeGitattributesEntry() function was not properly cleaning up
beads-related entries from .gitattributes. It only removed lines
containing "merge=beads" but left behind:
- The comment line "# Use bd merge for beads JSONL files"
- Empty lines following removed entries

This caused .gitattributes to remain in a modified state after
bd admin reset --force, triggering sync divergence warning loop.

The fix now:
- Skips lines containing "merge=beads" (existing behavior)
- Skips beads-related comment lines
- Skips empty lines that follow removed beads entries
- Properly cleans up file so it's either empty (and gets deleted)
  or contains only non-beads content

---------

Co-authored-by: Amp <amp@example.com>
This commit is contained in:
matt wilkie
2026-01-21 22:50:38 -07:00
committed by GitHub
parent 1d3ca43620
commit 1f8a6bf84e
13 changed files with 206 additions and 8908 deletions

View File

@@ -466,6 +466,80 @@ not valid json`
t.Error("Expected ClosedAt to be set for closed issue")
}
})
t.Run("migrate deleted status to tombstone (GH#1223)", func(t *testing.T) {
now := time.Now()
deletedAt := now.Format(time.RFC3339)
data := `{"id":"test-1","title":"Deleted Issue","status":"deleted","priority":1,"issue_type":"task","created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-01T00:00:00Z","deleted_at":"` + deletedAt + `"}`
notify := &testNotifier{}
issues, err := parseJSONL([]byte(data), notify)
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
if len(issues) != 1 {
t.Errorf("Expected 1 issue, got %d", len(issues))
}
if issues[0].Status != types.StatusTombstone {
t.Errorf("Expected status 'tombstone', got %s", issues[0].Status)
}
if issues[0].DeletedAt == nil {
t.Error("Expected DeletedAt to be set for migrated tombstone")
}
// Check that debug message was logged
if len(notify.debugs) == 0 {
t.Error("Expected debug notification for status migration")
}
})
t.Run("ensure tombstone has deleted_at", func(t *testing.T) {
data := `{"id":"test-1","title":"Tombstone Without DeletedAt","status":"tombstone","priority":1,"issue_type":"task","created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-01T00:00:00Z"}`
notify := &testNotifier{}
issues, err := parseJSONL([]byte(data), notify)
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
if issues[0].DeletedAt == nil {
t.Error("Expected DeletedAt to be auto-set for tombstone without deleted_at")
}
// Check that debug message was logged
if len(notify.debugs) == 0 {
t.Error("Expected debug notification for auto-added deleted_at")
}
})
t.Run("fix corrupted closed issue with deleted_at (GH#1223)", func(t *testing.T) {
now := time.Now()
deletedAt := now.Format(time.RFC3339)
data := `{"id":"bd-6s61","title":"Version Bump","status":"closed","priority":1,"issue_type":"task","created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-01T00:00:00Z","closed_at":"2024-01-02T00:00:00Z","deleted_at":"` + deletedAt + `"}`
notify := &testNotifier{}
issues, err := parseJSONL([]byte(data), notify)
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
if len(issues) != 1 {
t.Errorf("Expected 1 issue, got %d", len(issues))
}
// Issue should be converted to tombstone
if issues[0].Status != types.StatusTombstone {
t.Errorf("Expected status 'tombstone' for closed issue with deleted_at, got %s", issues[0].Status)
}
// Check that debug message was logged
if len(notify.debugs) == 0 {
t.Error("Expected debug notification for status correction")
}
})
}
func TestShowRemapping(t *testing.T) {