Files
beads/cmd/bd/doctor/fix/deletions_test.go
matt wilkie a22d949cbd Fix bd-in7q: prevent migrate-tombstones from corrupting deletions manifest (#554)
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'
2025-12-14 14:11:27 -08:00

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.