fix: support delete in --no-db mode (GH#822)

Add CreateTombstone() to MemoryStorage and deleteBatchFallback() to
handle deletion when SQLite is not available. This fixes the error
"tombstone operation not supported by this storage backend" when
using bd delete with --no-db flag.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beads/crew/emma
2025-12-31 11:45:37 -08:00
committed by Steve Yegge
parent ee51298fd5
commit b161e22144
4 changed files with 217 additions and 5 deletions

View File

@@ -7,6 +7,7 @@ import (
"testing"
"github.com/steveyegge/beads/internal/storage/memory"
"github.com/steveyegge/beads/internal/types"
)
// TestHandleDelete_DryRun verifies that dry-run mode returns what would be deleted
@@ -308,11 +309,15 @@ func TestHandleDelete_WithReason(t *testing.T) {
t.Fatalf("delete with reason failed: %s", resp.Error)
}
// Verify issue was deleted
// Verify issue was converted to tombstone (now that MemoryStorage supports CreateTombstone)
ctx := context.Background()
issue, _ := store.GetIssue(ctx, issueIDs[0])
if issue != nil {
t.Error("issue should have been deleted")
if issue == nil {
t.Error("issue should exist as tombstone")
} else if issue.Status != types.StatusTombstone {
t.Errorf("issue should be tombstone, got status=%s", issue.Status)
} else if issue.DeleteReason != "test deletion with reason" {
t.Errorf("expected DeleteReason='test deletion with reason', got '%s'", issue.DeleteReason)
}
}

View File

@@ -474,6 +474,41 @@ func (m *MemoryStorage) CloseIssue(ctx context.Context, id string, reason string
}, actor)
}
// CreateTombstone converts an existing issue to a tombstone record.
// This is a soft-delete that preserves the issue with status="tombstone".
func (m *MemoryStorage) CreateTombstone(ctx context.Context, id string, actor string, reason string) error {
m.mu.Lock()
defer m.mu.Unlock()
issue, ok := m.issues[id]
if !ok {
return fmt.Errorf("issue not found: %s", id)
}
now := time.Now()
issue.OriginalType = string(issue.IssueType)
issue.Status = types.StatusTombstone
issue.DeletedAt = &now
issue.DeletedBy = actor
issue.DeleteReason = reason
issue.UpdatedAt = now
// Mark as dirty for export
m.dirty[id] = true
// Record tombstone creation event
event := &types.Event{
IssueID: id,
EventType: "deleted",
Actor: actor,
Comment: &reason,
CreatedAt: now,
}
m.events[id] = append(m.events[id], event)
return nil
}
// DeleteIssue permanently deletes an issue and all associated data
func (m *MemoryStorage) DeleteIssue(ctx context.Context, id string) error {
m.mu.Lock()

View File

@@ -2,6 +2,7 @@ package memory
import (
"context"
"strings"
"testing"
"time"
@@ -949,6 +950,68 @@ func TestStatistics_TombstonesExcludedFromTotal(t *testing.T) {
}
}
func TestCreateTombstone(t *testing.T) {
store := setupTestMemory(t)
defer store.Close()
ctx := context.Background()
// Create an issue
issue := &types.Issue{
Title: "Test Issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
issueID := issue.ID
// Create tombstone
if err := store.CreateTombstone(ctx, issueID, "test-actor", "test deletion"); err != nil {
t.Fatalf("CreateTombstone failed: %v", err)
}
// Verify the issue is now a tombstone
updated, err := store.GetIssue(ctx, issueID)
if err != nil {
t.Fatalf("GetIssue failed: %v", err)
}
if updated.Status != types.StatusTombstone {
t.Errorf("Expected status=%s, got %s", types.StatusTombstone, updated.Status)
}
if updated.DeletedAt == nil {
t.Error("Expected DeletedAt to be set")
}
if updated.DeletedBy != "test-actor" {
t.Errorf("Expected DeletedBy=test-actor, got %s", updated.DeletedBy)
}
if updated.DeleteReason != "test deletion" {
t.Errorf("Expected DeleteReason='test deletion', got %s", updated.DeleteReason)
}
if updated.OriginalType != string(types.TypeTask) {
t.Errorf("Expected OriginalType=%s, got %s", types.TypeTask, updated.OriginalType)
}
}
func TestCreateTombstone_NotFound(t *testing.T) {
store := setupTestMemory(t)
defer store.Close()
ctx := context.Background()
// Try to create tombstone for non-existent issue
err := store.CreateTombstone(ctx, "nonexistent", "test", "reason")
if err == nil {
t.Fatal("Expected error for non-existent issue")
}
if !strings.Contains(err.Error(), "not found") {
t.Errorf("Expected 'not found' error, got: %v", err)
}
}
func TestConfigOperations(t *testing.T) {
store := setupTestMemory(t)
defer store.Close()