Files
beads/cmd/bd/doctor/fix/deletions_test.go
Steve Yegge 1e20d702f2 fix(doctor): include tombstones in getCurrentJSONLIDs to prevent corruption (#552)
The previous bd-in7q fix had backwards logic - by EXCLUDING tombstones
from currentIDs, they appeared missing when compared to historicalIDs,
causing HydrateDeletionsManifest to erroneously add them to deletions.jsonl.

This corruption manifested when:
1. Issues were migrated to tombstones via migrate-tombstones
2. Doctor hydration ran (directly or via sync)
3. Tombstones were seen as deleted and re-added to deletions.jsonl
4. Next import skipped thousands of issues with in deletions manifest

Fix: Include ALL issues (including tombstones) in currentIDs. Tombstones
represent migrated deletions that ARE accounted for - they should not
trigger new deletion records.

Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 17:33:07 -08:00

157 lines
4.2 KiB
Go

package fix
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/steveyegge/beads/internal/types"
)
// TestGetCurrentJSONLIDs_IncludesTombstones verifies that tombstones ARE included
// in the current ID set. This is critical for bd-552 fix: tombstones represent
// migrated deletions that are accounted for. By including them, they won't appear
// "missing" when compared to historicalIDs, preventing erroneous re-addition to
// deletions.jsonl.
func TestGetCurrentJSONLIDs_IncludesTombstones(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 contain ALL IDs including tombstones (bd-552 fix)
expectedIDs := map[string]bool{
"bd-abc": true,
"bd-def": true, // tombstone - must be included
"bd-ghi": true,
"bd-jkl": true, // tombstone - must be included
}
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 included (this is the bd-552 fix)
if !ids["bd-def"] {
t.Error("Tombstone bd-def MUST be included in current IDs (bd-552 fix)")
}
if !ids["bd-jkl"] {
t.Error("Tombstone bd-jkl MUST be included in current IDs (bd-552 fix)")
}
}
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 (bd-552: including tombstones in getCurrentJSONLIDs
// so they aren't erroneously re-added to deletions.jsonl).
// Integration tests are handled in migrate_tombstones_test.go with full sync cycle.