refactor: remove all deletions.jsonl code (bd-fom)
Complete removal of the legacy deletions.jsonl manifest system. Tombstones are now the sole deletion mechanism. Removed: - internal/deletions/ - entire package - cmd/bd/deleted.go - deleted command - cmd/bd/doctor/fix/deletions.go - HydrateDeletionsManifest - Tests for all removed functionality Cleaned: - cmd/bd/sync.go - removed sanitize, auto-compact - cmd/bd/delete.go - removed dual-writes - cmd/bd/doctor.go - removed checkDeletionsManifest - internal/importer/importer.go - removed deletions checks - internal/syncbranch/worktree.go - removed deletions merge - cmd/bd/integrity.go - updated validation (warn-only on decrease) Files removed: 12 Lines removed: ~7500 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/beads/internal/deletions"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
@@ -1075,88 +1074,6 @@ func TestConcurrentExternalRefImports(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckGitHistoryForDeletions_EmptyList(t *testing.T) {
|
||||
// Empty list should return nil
|
||||
result := checkGitHistoryForDeletions("/tmp/test", nil)
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil for empty list, got %v", result)
|
||||
}
|
||||
|
||||
result = checkGitHistoryForDeletions("/tmp/test", []string{})
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil for empty slice, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckGitHistoryForDeletions_NonGitDir(t *testing.T) {
|
||||
// Non-git directory should return empty (conservative behavior)
|
||||
tmpDir := t.TempDir()
|
||||
result := checkGitHistoryForDeletions(tmpDir, []string{"bd-test"})
|
||||
if len(result) != 0 {
|
||||
t.Errorf("Expected empty result for non-git dir, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWasEverInJSONL_NonGitDir(t *testing.T) {
|
||||
// Non-git directory should return false (conservative behavior)
|
||||
tmpDir := t.TempDir()
|
||||
result := wasEverInJSONL(tmpDir, ".beads/beads.jsonl", "bd-test")
|
||||
if result {
|
||||
t.Error("Expected false for non-git dir")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCheckGitHistory_NonGitDir(t *testing.T) {
|
||||
// Non-git directory should return empty (falls back to individual checks)
|
||||
tmpDir := t.TempDir()
|
||||
result := batchCheckGitHistory(tmpDir, ".beads/beads.jsonl", []string{"bd-test1", "bd-test2"})
|
||||
if len(result) != 0 {
|
||||
t.Errorf("Expected empty result for non-git dir, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertDeletionToTombstone(t *testing.T) {
|
||||
ts := time.Date(2025, 12, 5, 14, 30, 0, 0, time.UTC)
|
||||
del := deletions.DeletionRecord{
|
||||
ID: "bd-test",
|
||||
Timestamp: ts,
|
||||
Actor: "alice",
|
||||
Reason: "no longer needed",
|
||||
}
|
||||
|
||||
tombstone := convertDeletionToTombstone("bd-test", del)
|
||||
|
||||
if tombstone.ID != "bd-test" {
|
||||
t.Errorf("Expected ID 'bd-test', got %q", tombstone.ID)
|
||||
}
|
||||
if tombstone.Status != types.StatusTombstone {
|
||||
t.Errorf("Expected status 'tombstone', got %q", tombstone.Status)
|
||||
}
|
||||
if tombstone.Title != "(deleted)" {
|
||||
t.Errorf("Expected title '(deleted)', got %q", tombstone.Title)
|
||||
}
|
||||
if tombstone.DeletedAt == nil || !tombstone.DeletedAt.Equal(ts) {
|
||||
t.Errorf("Expected DeletedAt to be %v, got %v", ts, tombstone.DeletedAt)
|
||||
}
|
||||
if tombstone.DeletedBy != "alice" {
|
||||
t.Errorf("Expected DeletedBy 'alice', got %q", tombstone.DeletedBy)
|
||||
}
|
||||
if tombstone.DeleteReason != "no longer needed" {
|
||||
t.Errorf("Expected DeleteReason 'no longer needed', got %q", tombstone.DeleteReason)
|
||||
}
|
||||
if tombstone.OriginalType != "" {
|
||||
t.Errorf("Expected empty OriginalType, got %q", tombstone.OriginalType)
|
||||
}
|
||||
// Verify priority uses zero to indicate unknown (bd-9auw)
|
||||
if tombstone.Priority != 0 {
|
||||
t.Errorf("Expected Priority 0 (unknown), got %d", tombstone.Priority)
|
||||
}
|
||||
// IssueType must be valid for validation, so it defaults to task
|
||||
if tombstone.IssueType != types.TypeTask {
|
||||
t.Errorf("Expected IssueType 'task', got %q", tombstone.IssueType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportIssues_TombstoneFromJSONL(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -1225,182 +1142,6 @@ func TestImportIssues_TombstoneFromJSONL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportIssues_TombstoneNotFilteredByDeletionsManifest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
tmpDB := tmpDir + "/test.db"
|
||||
store, err := sqlite.New(context.Background(), tmpDB)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
|
||||
t.Fatalf("Failed to set prefix: %v", err)
|
||||
}
|
||||
|
||||
// Create a deletions manifest entry
|
||||
deletionsPath := deletions.DefaultPath(tmpDir)
|
||||
delRecord := deletions.DeletionRecord{
|
||||
ID: "test-abc123",
|
||||
Timestamp: time.Now().Add(-time.Hour),
|
||||
Actor: "alice",
|
||||
Reason: "old deletion",
|
||||
}
|
||||
if err := deletions.AppendDeletion(deletionsPath, delRecord); err != nil {
|
||||
t.Fatalf("Failed to write deletion record: %v", err)
|
||||
}
|
||||
|
||||
// Create a tombstone in JSONL for the same issue
|
||||
deletedAt := time.Now()
|
||||
tombstone := &types.Issue{
|
||||
ID: "test-abc123",
|
||||
Title: "(deleted)",
|
||||
Status: types.StatusTombstone,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeTask,
|
||||
CreatedAt: time.Now().Add(-24 * time.Hour),
|
||||
UpdatedAt: deletedAt,
|
||||
DeletedAt: &deletedAt,
|
||||
DeletedBy: "bob",
|
||||
DeleteReason: "JSONL tombstone",
|
||||
}
|
||||
|
||||
result, err := ImportIssues(ctx, tmpDB, store, []*types.Issue{tombstone}, Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("Import failed: %v", err)
|
||||
}
|
||||
|
||||
// The tombstone should be imported (not filtered by deletions manifest)
|
||||
if result.Created != 1 {
|
||||
t.Errorf("Expected 1 created (tombstone), got %d", result.Created)
|
||||
}
|
||||
if result.SkippedDeleted != 0 {
|
||||
t.Errorf("Expected 0 skipped deleted (tombstone should not be filtered), got %d", result.SkippedDeleted)
|
||||
}
|
||||
}
|
||||
|
||||
// TestImportIssues_LegacyDeletionsConvertedToTombstones tests that entries in
|
||||
// deletions.jsonl are converted to tombstones during import (bd-hp0m)
|
||||
func TestImportIssues_LegacyDeletionsConvertedToTombstones(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
tmpDB := tmpDir + "/test.db"
|
||||
store, err := sqlite.New(context.Background(), tmpDB)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
|
||||
t.Fatalf("Failed to set prefix: %v", err)
|
||||
}
|
||||
|
||||
// Create a deletions manifest with one entry
|
||||
deletionsPath := deletions.DefaultPath(tmpDir)
|
||||
deleteTime := time.Now().Add(-time.Hour)
|
||||
|
||||
del := deletions.DeletionRecord{
|
||||
ID: "test-abc",
|
||||
Timestamp: deleteTime,
|
||||
Actor: "alice",
|
||||
Reason: "duplicate of test-xyz",
|
||||
}
|
||||
if err := deletions.AppendDeletion(deletionsPath, del); err != nil {
|
||||
t.Fatalf("Failed to write deletion record: %v", err)
|
||||
}
|
||||
|
||||
// Create a regular issue (not in deletions)
|
||||
regularIssue := &types.Issue{
|
||||
ID: "test-def",
|
||||
Title: "Regular issue",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeTask,
|
||||
CreatedAt: time.Now().Add(-24 * time.Hour),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Create an issue that's in the deletions manifest (non-tombstone)
|
||||
deletedIssue := &types.Issue{
|
||||
ID: "test-abc",
|
||||
Title: "This will be skipped and converted",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeBug,
|
||||
CreatedAt: time.Now().Add(-48 * time.Hour),
|
||||
UpdatedAt: time.Now().Add(-2 * time.Hour),
|
||||
}
|
||||
|
||||
// Import both issues
|
||||
result, err := ImportIssues(ctx, tmpDB, store, []*types.Issue{regularIssue, deletedIssue}, Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("Import failed: %v", err)
|
||||
}
|
||||
|
||||
// Regular issue should be created
|
||||
// The deleted issue is skipped (in deletions manifest), but a tombstone is created from deletions.jsonl
|
||||
// So we expect: 1 regular + 1 tombstone = 2 created
|
||||
if result.Created != 2 {
|
||||
t.Errorf("Expected 2 created (1 regular + 1 tombstone from deletions.jsonl), got %d", result.Created)
|
||||
}
|
||||
if result.SkippedDeleted != 1 {
|
||||
t.Errorf("Expected 1 skipped deleted (issue in deletions.jsonl), got %d", result.SkippedDeleted)
|
||||
}
|
||||
// Verify ConvertedToTombstone counter (bd-wucl)
|
||||
if result.ConvertedToTombstone != 1 {
|
||||
t.Errorf("Expected 1 converted to tombstone, got %d", result.ConvertedToTombstone)
|
||||
}
|
||||
if len(result.ConvertedTombstoneIDs) != 1 || result.ConvertedTombstoneIDs[0] != "test-abc" {
|
||||
t.Errorf("Expected ConvertedTombstoneIDs [test-abc], got %v", result.ConvertedTombstoneIDs)
|
||||
}
|
||||
|
||||
// Verify regular issue was imported
|
||||
issues, err := store.SearchIssues(ctx, "", types.IssueFilter{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to search issues: %v", err)
|
||||
}
|
||||
foundRegular := false
|
||||
for _, i := range issues {
|
||||
if i.ID == "test-def" {
|
||||
foundRegular = true
|
||||
}
|
||||
}
|
||||
if !foundRegular {
|
||||
t.Error("Regular issue not found after import")
|
||||
}
|
||||
|
||||
// Verify tombstone was created from deletions.jsonl
|
||||
allIssues, err := store.SearchIssues(ctx, "", types.IssueFilter{IncludeTombstones: true})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to search all issues: %v", err)
|
||||
}
|
||||
|
||||
var tombstone *types.Issue
|
||||
for _, i := range allIssues {
|
||||
if i.ID == "test-abc" {
|
||||
tombstone = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// test-abc should be a tombstone (was in JSONL and deletions)
|
||||
if tombstone == nil {
|
||||
t.Fatal("Expected tombstone for test-abc not found")
|
||||
}
|
||||
if tombstone.Status != types.StatusTombstone {
|
||||
t.Errorf("Expected test-abc to be tombstone, got status %q", tombstone.Status)
|
||||
}
|
||||
if tombstone.DeletedBy != "alice" {
|
||||
t.Errorf("Expected DeletedBy 'alice', got %q", tombstone.DeletedBy)
|
||||
}
|
||||
if tombstone.DeleteReason != "duplicate of test-xyz" {
|
||||
t.Errorf("Expected DeleteReason 'duplicate of test-xyz', got %q", tombstone.DeleteReason)
|
||||
}
|
||||
}
|
||||
|
||||
// TestImportOrphanSkip_CountMismatch verifies that orphaned issues are properly
|
||||
// skipped during import and tracked in the result count (bd-ckej).
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user