package rpc import ( "context" "encoding/json" "path/filepath" "testing" "github.com/steveyegge/beads/internal/storage/memory" ) // TestHandleDelete_DryRun verifies that dry-run mode returns what would be deleted // without actually deleting the issues func TestHandleDelete_DryRun(t *testing.T) { store := memory.New("/tmp/test.jsonl") server := NewServer("/tmp/test.sock", store, "/tmp", "/tmp/test.db") // Create test issues issueIDs := createTestIssues(t, server, 3) // Request dry-run deletion deleteArgs := DeleteArgs{ IDs: issueIDs, DryRun: true, } deleteJSON, _ := json.Marshal(deleteArgs) deleteReq := &Request{ Operation: OpDelete, Args: deleteJSON, Actor: "test-user", } resp := server.handleDelete(deleteReq) if !resp.Success { t.Fatalf("dry-run delete failed: %s", resp.Error) } // Parse response var result map[string]interface{} if err := json.Unmarshal(resp.Data, &result); err != nil { t.Fatalf("failed to parse response: %v", err) } // Verify dry-run flag in response if dryRun, ok := result["dry_run"].(bool); !ok || !dryRun { t.Error("expected dry_run: true in response") } // Verify issue count if count, ok := result["issue_count"].(float64); !ok || int(count) != 3 { t.Errorf("expected issue_count: 3, got %v", result["issue_count"]) } // Verify issues are still present (not actually deleted) ctx := context.Background() for _, id := range issueIDs { issue, err := store.GetIssue(ctx, id) if err != nil { t.Errorf("issue %s should still exist after dry-run, got error: %v", id, err) } if issue == nil { t.Errorf("issue %s should still exist after dry-run, but was deleted", id) } } } // TestHandleDelete_InvalidIssueID verifies error handling for non-existent issue IDs func TestHandleDelete_InvalidIssueID(t *testing.T) { store := memory.New("/tmp/test.jsonl") server := NewServer("/tmp/test.sock", store, "/tmp", "/tmp/test.db") // Try to delete non-existent issue deleteArgs := DeleteArgs{ IDs: []string{"bd-nonexistent"}, } deleteJSON, _ := json.Marshal(deleteArgs) deleteReq := &Request{ Operation: OpDelete, Args: deleteJSON, Actor: "test-user", } resp := server.handleDelete(deleteReq) // Should fail since all deletes failed if resp.Success { t.Error("expected failure for non-existent issue ID") } if resp.Error == "" { t.Error("expected error message for failed deletion") } } // TestHandleDelete_PartialSuccess verifies behavior when some IDs are valid and others aren't func TestHandleDelete_PartialSuccess(t *testing.T) { store := memory.New("/tmp/test.jsonl") server := NewServer("/tmp/test.sock", store, "/tmp", "/tmp/test.db") // Create one valid issue validIDs := createTestIssues(t, server, 1) validID := validIDs[0] // Mix valid and invalid IDs deleteArgs := DeleteArgs{ IDs: []string{validID, "bd-fake1", "bd-fake2"}, } deleteJSON, _ := json.Marshal(deleteArgs) deleteReq := &Request{ Operation: OpDelete, Args: deleteJSON, Actor: "test-user", } resp := server.handleDelete(deleteReq) // Should succeed (partial success) if !resp.Success { t.Errorf("expected partial success, got error: %s", resp.Error) } // Parse response var result map[string]interface{} if err := json.Unmarshal(resp.Data, &result); err != nil { t.Fatalf("failed to parse response: %v", err) } // Verify counts if deleted, ok := result["deleted_count"].(float64); !ok || int(deleted) != 1 { t.Errorf("expected deleted_count: 1, got %v", result["deleted_count"]) } if total, ok := result["total_count"].(float64); !ok || int(total) != 3 { t.Errorf("expected total_count: 3, got %v", result["total_count"]) } // Verify errors array exists if errors, ok := result["errors"].([]interface{}); !ok || len(errors) != 2 { t.Errorf("expected 2 errors in response, got %v", result["errors"]) } // Verify partial_success flag if partial, ok := result["partial_success"].(bool); !ok || !partial { t.Error("expected partial_success: true in response") } } // TestHandleDelete_NoIDs verifies error when no issue IDs are provided func TestHandleDelete_NoIDs(t *testing.T) { store := memory.New("/tmp/test.jsonl") server := NewServer("/tmp/test.sock", store, "/tmp", "/tmp/test.db") // Try to delete with empty IDs array deleteArgs := DeleteArgs{ IDs: []string{}, } deleteJSON, _ := json.Marshal(deleteArgs) deleteReq := &Request{ Operation: OpDelete, Args: deleteJSON, Actor: "test-user", } resp := server.handleDelete(deleteReq) if resp.Success { t.Error("expected failure when no IDs provided") } if resp.Error != "no issue IDs provided for deletion" { t.Errorf("unexpected error message: %s", resp.Error) } } // TestHandleDelete_StorageNotAvailable verifies error when storage is nil func TestHandleDelete_StorageNotAvailable(t *testing.T) { // Create server without storage server := NewServer("/tmp/test.sock", nil, "/tmp", "/tmp/test.db") deleteArgs := DeleteArgs{ IDs: []string{"bd-123"}, } deleteJSON, _ := json.Marshal(deleteArgs) deleteReq := &Request{ Operation: OpDelete, Args: deleteJSON, Actor: "test-user", } resp := server.handleDelete(deleteReq) if resp.Success { t.Error("expected failure when storage not available") } if resp.Error == "" { t.Error("expected error message about storage not available") } } // TestHandleDelete_InvalidJSON verifies error handling for malformed JSON args func TestHandleDelete_InvalidJSON(t *testing.T) { store := memory.New("/tmp/test.jsonl") server := NewServer("/tmp/test.sock", store, "/tmp", "/tmp/test.db") deleteReq := &Request{ Operation: OpDelete, Args: []byte("not valid json"), Actor: "test-user", } resp := server.handleDelete(deleteReq) if resp.Success { t.Error("expected failure for invalid JSON") } if resp.Error == "" { t.Error("expected error message for invalid JSON") } } // TestHandleDelete_ResponseStructure verifies the response format for successful deletion func TestHandleDelete_ResponseStructure(t *testing.T) { store := memory.New("/tmp/test.jsonl") server := NewServer("/tmp/test.sock", store, "/tmp", "/tmp/test.db") // Create test issues issueIDs := createTestIssues(t, server, 2) // Delete issues deleteArgs := DeleteArgs{ IDs: issueIDs, Reason: "testing response structure", } deleteJSON, _ := json.Marshal(deleteArgs) deleteReq := &Request{ Operation: OpDelete, Args: deleteJSON, Actor: "test-user", } resp := server.handleDelete(deleteReq) if !resp.Success { t.Fatalf("delete failed: %s", resp.Error) } // Parse response var result map[string]interface{} if err := json.Unmarshal(resp.Data, &result); err != nil { t.Fatalf("failed to parse response: %v", err) } // Verify required fields if _, ok := result["deleted_count"]; !ok { t.Error("response missing 'deleted_count' field") } if _, ok := result["total_count"]; !ok { t.Error("response missing 'total_count' field") } // Verify counts match deleted := result["deleted_count"].(float64) total := result["total_count"].(float64) if int(deleted) != 2 { t.Errorf("expected deleted_count: 2, got %d", int(deleted)) } if int(total) != 2 { t.Errorf("expected total_count: 2, got %d", int(total)) } // Should not have errors field when all succeed if _, ok := result["errors"]; ok { t.Error("should not have 'errors' field when all deletions succeed") } // Should not have partial_success when all succeed if _, ok := result["partial_success"]; ok { t.Error("should not have 'partial_success' field when all deletions succeed") } } // TestHandleDelete_WithReason verifies deletion with a reason func TestHandleDelete_WithReason(t *testing.T) { store := memory.New("/tmp/test.jsonl") server := NewServer("/tmp/test.sock", store, "/tmp", "/tmp/test.db") // Create test issue issueIDs := createTestIssues(t, server, 1) // Delete with reason deleteArgs := DeleteArgs{ IDs: issueIDs, Reason: "test deletion with reason", } deleteJSON, _ := json.Marshal(deleteArgs) deleteReq := &Request{ Operation: OpDelete, Args: deleteJSON, Actor: "test-user", } resp := server.handleDelete(deleteReq) if !resp.Success { t.Fatalf("delete with reason failed: %s", resp.Error) } // Verify issue was deleted ctx := context.Background() issue, _ := store.GetIssue(ctx, issueIDs[0]) if issue != nil { t.Error("issue should have been deleted") } } // TestHandleDelete_WithTombstone tests delete handler with SQLite storage that supports tombstones func TestHandleDelete_WithTombstone(t *testing.T) { dbPath := filepath.Join(t.TempDir(), "test.db") store := newTestStore(t, dbPath) defer store.Close() server := NewServer("/tmp/test.sock", store, "/tmp", dbPath) // Create a test issue using the SQLite store ctx := context.Background() createArgs := CreateArgs{ Title: "Issue for tombstone test", IssueType: "task", Priority: 1, } createJSON, _ := json.Marshal(createArgs) createReq := &Request{ Operation: OpCreate, Args: createJSON, Actor: "test-user", } createResp := server.handleCreate(createReq) if !createResp.Success { t.Fatalf("failed to create test issue: %s", createResp.Error) } var createdIssue map[string]interface{} if err := json.Unmarshal(createResp.Data, &createdIssue); err != nil { t.Fatalf("failed to parse created issue: %v", err) } issueID := createdIssue["id"].(string) // Delete the issue (should create tombstone) deleteArgs := DeleteArgs{ IDs: []string{issueID}, Reason: "tombstone test", } deleteJSON, _ := json.Marshal(deleteArgs) deleteReq := &Request{ Operation: OpDelete, Args: deleteJSON, Actor: "test-user", } deleteResp := server.handleDelete(deleteReq) if !deleteResp.Success { t.Fatalf("delete failed: %s", deleteResp.Error) } // Verify issue was tombstoned (still exists but with tombstone status) issue, err := store.GetIssue(ctx, issueID) if err != nil { t.Fatalf("failed to get tombstoned issue: %v", err) } if issue == nil { t.Fatal("tombstoned issue should still exist in database") } if issue.Status != "tombstone" { t.Errorf("expected status=tombstone, got %s", issue.Status) } if issue.DeletedAt == nil { t.Error("DeletedAt should be set for tombstoned issue") } if issue.DeleteReason != "tombstone test" { t.Errorf("expected DeleteReason='tombstone test', got %q", issue.DeleteReason) } } // TestHandleDelete_AllFail verifies behavior when all deletions fail func TestHandleDelete_AllFail(t *testing.T) { store := memory.New("/tmp/test.jsonl") server := NewServer("/tmp/test.sock", store, "/tmp", "/tmp/test.db") // Try to delete multiple non-existent issues deleteArgs := DeleteArgs{ IDs: []string{"bd-fake1", "bd-fake2", "bd-fake3"}, } deleteJSON, _ := json.Marshal(deleteArgs) deleteReq := &Request{ Operation: OpDelete, Args: deleteJSON, Actor: "test-user", } resp := server.handleDelete(deleteReq) // Should fail since all deletes failed if resp.Success { t.Error("expected failure when all deletions fail") } if resp.Error == "" { t.Error("expected error message when all deletions fail") } } // TestHandleDelete_DryRunPreservesData verifies dry-run doesn't modify anything func TestHandleDelete_DryRunPreservesData(t *testing.T) { store := memory.New("/tmp/test.jsonl") server := NewServer("/tmp/test.sock", store, "/tmp", "/tmp/test.db") // Create test issues issueIDs := createTestIssues(t, server, 3) // Get issues before dry-run ctx := context.Background() beforeIssues := make(map[string]string) for _, id := range issueIDs { issue, _ := store.GetIssue(ctx, id) if issue != nil { beforeIssues[id] = issue.Title } } // Do dry-run deletion multiple times for i := 0; i < 3; i++ { deleteArgs := DeleteArgs{ IDs: issueIDs, DryRun: true, } deleteJSON, _ := json.Marshal(deleteArgs) deleteReq := &Request{ Operation: OpDelete, Args: deleteJSON, Actor: "test-user", } resp := server.handleDelete(deleteReq) if !resp.Success { t.Fatalf("dry-run %d failed: %s", i, resp.Error) } } // Verify all issues still exist with same data for id, title := range beforeIssues { issue, err := store.GetIssue(ctx, id) if err != nil { t.Errorf("issue %s disappeared after dry-runs: %v", id, err) continue } if issue == nil { t.Errorf("issue %s was deleted by dry-run", id) continue } if issue.Title != title { t.Errorf("issue %s title changed: expected %q, got %q", id, title, issue.Title) } } } // createTestIssues is a helper to create test issues and return their IDs func createTestIssues(t *testing.T, server *Server, count int) []string { t.Helper() ids := make([]string, count) for i := 0; i < count; i++ { createArgs := CreateArgs{ Title: "Test Issue for Delete", IssueType: "task", Priority: 1, } createJSON, _ := json.Marshal(createArgs) createReq := &Request{ Operation: OpCreate, Args: createJSON, Actor: "test-user", } createResp := server.handleCreate(createReq) if !createResp.Success { t.Fatalf("failed to create test issue %d: %s", i, createResp.Error) } var createdIssue map[string]interface{} if err := json.Unmarshal(createResp.Data, &createdIssue); err != nil { t.Fatalf("failed to parse created issue %d: %v", i, err) } ids[i] = createdIssue["id"].(string) } return ids }