Root cause: bd doctor hydrate was re-adding migrated tombstones to the deletions manifest because getCurrentJSONLIDs() included all issues, including tombstones. When compared against git history, tombstones appeared as 'deleted' and were incorrectly added to the manifest as new deletions, corrupting the database on next sync. Fix: Skip tombstone-status issues in getCurrentJSONLIDs() so they don't participate in deletion detection. Tombstones represent already-recorded deletions/migrations and shouldn't be treated as active issues. Changes: - cmd/bd/doctor/fix/deletions.go: Skip tombstones in getCurrentJSONLIDs() - cmd/bd/doctor/fix/deletions_test.go: New tests for tombstone skipping - cmd/bd/migrate_tombstones_test.go: Test that tombstones are valid This fixes the bug where 'bd migrate-tombstones' followed by 'bd sync' would add thousands of deletion records with author 'bd-doctor-hydrate'
149 lines
3.7 KiB
Go
149 lines
3.7 KiB
Go
package fix
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/steveyegge/beads/internal/types"
|
|
)
|
|
|
|
func TestGetCurrentJSONLIDs_SkipsTombstones(t *testing.T) {
|
|
// Setup: Create temp file with mix of normal issues and tombstones
|
|
tmpDir := t.TempDir()
|
|
jsonlPath := filepath.Join(tmpDir, "issues.jsonl")
|
|
|
|
// Create a JSONL file with both normal issues and tombstones
|
|
issues := []*types.Issue{
|
|
{
|
|
ID: "bd-abc",
|
|
Title: "Normal issue",
|
|
Status: types.StatusOpen,
|
|
},
|
|
{
|
|
ID: "bd-def",
|
|
Title: "(deleted)",
|
|
Status: types.StatusTombstone,
|
|
DeletedBy: "test-user",
|
|
},
|
|
{
|
|
ID: "bd-ghi",
|
|
Title: "Another normal issue",
|
|
Status: types.StatusOpen,
|
|
},
|
|
{
|
|
ID: "bd-jkl",
|
|
Title: "(deleted)",
|
|
Status: types.StatusTombstone,
|
|
DeletedBy: "test-user",
|
|
},
|
|
}
|
|
|
|
file, err := os.Create(jsonlPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test JSONL file: %v", err)
|
|
}
|
|
encoder := json.NewEncoder(file)
|
|
for _, issue := range issues {
|
|
if err := encoder.Encode(issue); err != nil {
|
|
file.Close()
|
|
t.Fatalf("Failed to write issue to JSONL: %v", err)
|
|
}
|
|
}
|
|
file.Close()
|
|
|
|
// Call getCurrentJSONLIDs
|
|
ids, err := getCurrentJSONLIDs(jsonlPath)
|
|
if err != nil {
|
|
t.Fatalf("getCurrentJSONLIDs failed: %v", err)
|
|
}
|
|
|
|
// Verify: Should only contain non-tombstone IDs
|
|
expectedIDs := map[string]bool{
|
|
"bd-abc": true,
|
|
"bd-ghi": true,
|
|
}
|
|
|
|
if len(ids) != len(expectedIDs) {
|
|
t.Errorf("Expected %d IDs, got %d. IDs: %v", len(expectedIDs), len(ids), ids)
|
|
}
|
|
|
|
for expectedID := range expectedIDs {
|
|
if !ids[expectedID] {
|
|
t.Errorf("Expected ID %s to be present", expectedID)
|
|
}
|
|
}
|
|
|
|
// Verify tombstones are NOT included
|
|
if ids["bd-def"] {
|
|
t.Error("Tombstone bd-def should not be included in current IDs")
|
|
}
|
|
if ids["bd-jkl"] {
|
|
t.Error("Tombstone bd-jkl should not be included in current IDs")
|
|
}
|
|
}
|
|
|
|
func TestGetCurrentJSONLIDs_HandlesEmptyFile(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
jsonlPath := filepath.Join(tmpDir, "issues.jsonl")
|
|
|
|
// Create empty file
|
|
if _, err := os.Create(jsonlPath); err != nil {
|
|
t.Fatalf("Failed to create empty file: %v", err)
|
|
}
|
|
|
|
ids, err := getCurrentJSONLIDs(jsonlPath)
|
|
if err != nil {
|
|
t.Fatalf("getCurrentJSONLIDs failed: %v", err)
|
|
}
|
|
|
|
if len(ids) != 0 {
|
|
t.Errorf("Expected 0 IDs from empty file, got %d", len(ids))
|
|
}
|
|
}
|
|
|
|
func TestGetCurrentJSONLIDs_HandlesMissingFile(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
nonexistentPath := filepath.Join(tmpDir, "nonexistent.jsonl")
|
|
|
|
ids, err := getCurrentJSONLIDs(nonexistentPath)
|
|
if err != nil {
|
|
t.Fatalf("getCurrentJSONLIDs should handle missing file gracefully: %v", err)
|
|
}
|
|
|
|
if len(ids) != 0 {
|
|
t.Errorf("Expected 0 IDs from missing file, got %d", len(ids))
|
|
}
|
|
}
|
|
|
|
func TestGetCurrentJSONLIDs_SkipsInvalidJSON(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
jsonlPath := filepath.Join(tmpDir, "issues.jsonl")
|
|
|
|
// Write mixed valid and invalid JSON lines
|
|
content := `{"id":"bd-valid","status":"open"}
|
|
invalid json line
|
|
{"id":"bd-another","status":"open"}
|
|
`
|
|
if err := os.WriteFile(jsonlPath, []byte(content), 0600); err != nil {
|
|
t.Fatalf("Failed to write test file: %v", err)
|
|
}
|
|
|
|
ids, err := getCurrentJSONLIDs(jsonlPath)
|
|
if err != nil {
|
|
t.Fatalf("getCurrentJSONLIDs failed: %v", err)
|
|
}
|
|
|
|
if len(ids) != 2 {
|
|
t.Errorf("Expected 2 valid IDs, got %d. IDs: %v", len(ids), ids)
|
|
}
|
|
if !ids["bd-valid"] || !ids["bd-another"] {
|
|
t.Error("Expected to parse both valid issues despite invalid line in between")
|
|
}
|
|
}
|
|
|
|
// Note: Full integration test for HydrateDeletionsManifest would require git repo setup.
|
|
// The unit tests above verify the core fix (skipping tombstones in getCurrentJSONLIDs).
|
|
// Integration tests are handled in migrate_tombstones_test.go with full sync cycle.
|