Files
beads/cmd/bd/delete_rpc_test.go
Steve Yegge 1611f16751 refactor: remove unused bd pin/unpin/hook commands (bd-x0zl)
Analysis found these commands are dead code:
- gt never calls `bd pin` - uses `bd update --status=pinned` instead
- Beads.Pin() wrapper exists but is never called
- bd hook functionality duplicated by gt mol status
- Code comment says "pinned field is cosmetic for bd hook visibility"

Removed:
- cmd/bd/pin.go
- cmd/bd/unpin.go
- cmd/bd/hook.go

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 16:02:15 -08:00

937 lines
24 KiB
Go

//go:build integration
// +build integration
package main
import (
"bytes"
"context"
"encoding/json"
"io"
"log/slog"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types"
)
// TestDeleteViaDaemon_SuccessfulDeletion tests successful single issue deletion via daemon RPC
func TestDeleteViaDaemon_SuccessfulDeletion(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
ctx, cancel, client, testStore, cleanup := setupDaemonTestEnvForDelete(t)
defer cleanup()
defer cancel()
// Create a test issue
issue := &types.Issue{
Title: "Test Issue for Deletion",
IssueType: "task",
Status: types.StatusOpen,
Priority: 2,
}
if err := testStore.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create test issue: %v", err)
}
// Delete via daemon RPC
deleteArgs := &rpc.DeleteArgs{
IDs: []string{issue.ID},
Force: true,
DryRun: false,
Reason: "test deletion",
}
resp, err := client.Delete(deleteArgs)
if err != nil {
t.Fatalf("Delete RPC failed: %v", err)
}
if !resp.Success {
t.Fatalf("Delete failed: %s", resp.Error)
}
// Verify the response data
var result map[string]interface{}
if err := json.Unmarshal(resp.Data, &result); err != nil {
t.Fatalf("Failed to parse response: %v", err)
}
deletedCount := int(result["deleted_count"].(float64))
if deletedCount != 1 {
t.Errorf("Expected 1 deletion, got %d", deletedCount)
}
// Verify issue is actually deleted (tombstoned)
deletedIssue, err := testStore.GetIssue(ctx, issue.ID)
if err != nil {
t.Fatalf("GetIssue failed: %v", err)
}
if deletedIssue != nil && deletedIssue.Status != types.StatusTombstone {
t.Errorf("Issue should be tombstoned, got status: %s", deletedIssue.Status)
}
}
// TestDeleteViaDaemon_CascadeDeletion tests cascade deletion through daemon
func TestDeleteViaDaemon_CascadeDeletion(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
ctx, cancel, client, testStore, cleanup := setupDaemonTestEnvForDelete(t)
defer cleanup()
defer cancel()
// Create parent and child issues
parentIssue := &types.Issue{
Title: "Parent Issue",
IssueType: "epic",
Status: types.StatusOpen,
Priority: 1,
}
if err := testStore.CreateIssue(ctx, parentIssue, "test"); err != nil {
t.Fatalf("Failed to create parent issue: %v", err)
}
childIssue := &types.Issue{
Title: "Child Issue",
IssueType: "task",
Status: types.StatusOpen,
Priority: 2,
}
if err := testStore.CreateIssue(ctx, childIssue, "test"); err != nil {
t.Fatalf("Failed to create child issue: %v", err)
}
// Create dependency: child depends on parent
dep := &types.Dependency{
IssueID: childIssue.ID,
DependsOnID: parentIssue.ID,
Type: types.DepBlocks,
}
if err := testStore.AddDependency(ctx, dep, "test"); err != nil {
t.Fatalf("Failed to add dependency: %v", err)
}
// Delete parent with cascade
deleteArgs := &rpc.DeleteArgs{
IDs: []string{parentIssue.ID},
Force: true,
Cascade: true,
DryRun: false,
Reason: "cascade deletion test",
}
resp, err := client.Delete(deleteArgs)
if err != nil {
t.Fatalf("Delete RPC failed: %v", err)
}
if !resp.Success {
t.Fatalf("Cascade delete failed: %s", resp.Error)
}
var result map[string]interface{}
if err := json.Unmarshal(resp.Data, &result); err != nil {
t.Fatalf("Failed to parse response: %v", err)
}
deletedCount := int(result["deleted_count"].(float64))
// Cascade should delete both parent and dependent child
if deletedCount < 1 {
t.Errorf("Expected at least 1 deletion in cascade, got %d", deletedCount)
}
}
// TestDeleteViaDaemon_ForceDeletion tests force deletion bypassing dependency checks
func TestDeleteViaDaemon_ForceDeletion(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
ctx, cancel, client, testStore, cleanup := setupDaemonTestEnvForDelete(t)
defer cleanup()
defer cancel()
// Create issues with dependencies
issue1 := &types.Issue{
Title: "Issue 1",
IssueType: "task",
Status: types.StatusOpen,
Priority: 2,
}
if err := testStore.CreateIssue(ctx, issue1, "test"); err != nil {
t.Fatalf("Failed to create issue1: %v", err)
}
issue2 := &types.Issue{
Title: "Issue 2 depends on Issue 1",
IssueType: "task",
Status: types.StatusOpen,
Priority: 2,
}
if err := testStore.CreateIssue(ctx, issue2, "test"); err != nil {
t.Fatalf("Failed to create issue2: %v", err)
}
// Create dependency
dep := &types.Dependency{
IssueID: issue2.ID,
DependsOnID: issue1.ID,
Type: types.DepBlocks,
}
if err := testStore.AddDependency(ctx, dep, "test"); err != nil {
t.Fatalf("Failed to add dependency: %v", err)
}
// Force delete issue1 (which has dependents)
deleteArgs := &rpc.DeleteArgs{
IDs: []string{issue1.ID},
Force: true,
DryRun: false,
Reason: "force deletion test",
}
resp, err := client.Delete(deleteArgs)
if err != nil {
t.Fatalf("Delete RPC failed: %v", err)
}
if !resp.Success {
t.Fatalf("Force delete failed: %s", resp.Error)
}
var result map[string]interface{}
if err := json.Unmarshal(resp.Data, &result); err != nil {
t.Fatalf("Failed to parse response: %v", err)
}
deletedCount := int(result["deleted_count"].(float64))
if deletedCount != 1 {
t.Errorf("Expected 1 deletion, got %d", deletedCount)
}
}
// TestDeleteViaDaemon_DryRunMode tests dry-run mode with no actual deletion
func TestDeleteViaDaemon_DryRunMode(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
ctx, cancel, client, testStore, cleanup := setupDaemonTestEnvForDelete(t)
defer cleanup()
defer cancel()
// Create a test issue
issue := &types.Issue{
Title: "Issue for DryRun Test",
IssueType: "task",
Status: types.StatusOpen,
Priority: 2,
}
if err := testStore.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create issue: %v", err)
}
// Dry-run delete
deleteArgs := &rpc.DeleteArgs{
IDs: []string{issue.ID},
Force: true,
DryRun: true,
Reason: "dry run test",
}
resp, err := client.Delete(deleteArgs)
if err != nil {
t.Fatalf("Delete RPC failed: %v", err)
}
if !resp.Success {
t.Fatalf("DryRun delete failed: %s", resp.Error)
}
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 response structure
if _, ok := result["issue_count"]; !ok {
// Check alternative field names used in dry-run responses
if _, ok := result["deleted_count"]; !ok {
t.Logf("DryRun response: %+v", result)
}
}
// Verify issue still exists (not deleted in dry-run)
existingIssue, err := testStore.GetIssue(ctx, issue.ID)
if err != nil {
t.Fatalf("GetIssue failed: %v", err)
}
if existingIssue == nil {
t.Error("Issue should still exist after dry-run")
}
if existingIssue != nil && existingIssue.Status == types.StatusTombstone {
t.Error("Issue should not be tombstoned in dry-run mode")
}
}
// TestDeleteViaDaemon_InvalidIssueID tests error handling for invalid issue IDs
func TestDeleteViaDaemon_InvalidIssueID(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
_, cancel, client, _, cleanup := setupDaemonTestEnvForDelete(t)
defer cleanup()
defer cancel()
// Try to delete non-existent issue
deleteArgs := &rpc.DeleteArgs{
IDs: []string{"test-nonexistent-xxx"},
Force: true,
DryRun: false,
Reason: "invalid id test",
}
resp, err := client.Delete(deleteArgs)
// The RPC call should succeed but the response should indicate failure
if err == nil && resp != nil {
if resp.Success {
// Parse response to check for errors field
var result map[string]interface{}
if err := json.Unmarshal(resp.Data, &result); err == nil {
if errors, ok := result["errors"].([]interface{}); ok && len(errors) > 0 {
t.Logf("Got expected errors for invalid ID: %v", errors)
return
}
// Check deleted_count
if deletedCount, ok := result["deleted_count"].(float64); ok && deletedCount == 0 {
t.Logf("Got expected 0 deletions for invalid ID")
return
}
}
}
}
// Both error or failure response are acceptable for invalid IDs
t.Logf("Delete of invalid ID handled: err=%v, resp=%+v", err, resp)
}
// TestDeleteViaDaemon_BatchDeletion tests deleting multiple issues at once
func TestDeleteViaDaemon_BatchDeletion(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
ctx, cancel, client, testStore, cleanup := setupDaemonTestEnvForDelete(t)
defer cleanup()
defer cancel()
// Create multiple test issues
var issueIDs []string
for i := 0; i < 3; i++ {
issue := &types.Issue{
Title: "Batch Issue " + string(rune('A'+i)),
IssueType: "task",
Status: types.StatusOpen,
Priority: 2,
}
if err := testStore.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create issue %d: %v", i, err)
}
issueIDs = append(issueIDs, issue.ID)
}
// Batch delete
deleteArgs := &rpc.DeleteArgs{
IDs: issueIDs,
Force: true,
DryRun: false,
Reason: "batch deletion test",
}
resp, err := client.Delete(deleteArgs)
if err != nil {
t.Fatalf("Delete RPC failed: %v", err)
}
if !resp.Success {
t.Fatalf("Batch delete failed: %s", resp.Error)
}
var result map[string]interface{}
if err := json.Unmarshal(resp.Data, &result); err != nil {
t.Fatalf("Failed to parse response: %v", err)
}
deletedCount := int(result["deleted_count"].(float64))
if deletedCount != 3 {
t.Errorf("Expected 3 deletions, got %d", deletedCount)
}
}
// TestDeleteViaDaemon_JSONOutput tests JSON output formatting
func TestDeleteViaDaemon_JSONOutput(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
ctx, cancel, client, testStore, cleanup := setupDaemonTestEnvForDelete(t)
defer cleanup()
defer cancel()
// Create a test issue
issue := &types.Issue{
Title: "Issue for JSON Output Test",
IssueType: "task",
Status: types.StatusOpen,
Priority: 2,
}
if err := testStore.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create issue: %v", err)
}
// Delete via daemon
deleteArgs := &rpc.DeleteArgs{
IDs: []string{issue.ID},
Force: true,
DryRun: false,
Reason: "json output test",
}
resp, err := client.Delete(deleteArgs)
if err != nil {
t.Fatalf("Delete RPC failed: %v", err)
}
if !resp.Success {
t.Fatalf("Delete failed: %s", resp.Error)
}
// Validate JSON structure
var result map[string]interface{}
if err := json.Unmarshal(resp.Data, &result); err != nil {
t.Fatalf("Response is not valid JSON: %v", err)
}
// Check expected fields in JSON response
expectedFields := []string{"deleted_count", "total_count"}
for _, field := range expectedFields {
if _, ok := result[field]; !ok {
t.Errorf("Expected field %q in JSON response, got: %+v", field, result)
}
}
}
// TestDeleteViaDaemon_HumanReadableOutput tests the human-readable output formatting
func TestDeleteViaDaemon_HumanReadableOutput(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
ctx, cancel, _, testStore, cleanup := setupDaemonTestEnvForDelete(t)
defer cleanup()
defer cancel()
// Create a test issue
issue := &types.Issue{
Title: "Issue for Human Output Test",
IssueType: "task",
Status: types.StatusOpen,
Priority: 2,
}
if err := testStore.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create issue: %v", err)
}
// Test output formatting by capturing stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Create a mock response to test output formatting
result := map[string]interface{}{
"deleted_count": float64(1),
"total_count": float64(1),
}
resultJSON, _ := json.Marshal(result)
// Simulate the human-readable output logic
deletedCount := int(result["deleted_count"].(float64))
if deletedCount == 1 {
os.Stdout.WriteString("✓ Deleted " + issue.ID + "\n")
} else {
os.Stdout.WriteString("✓ Deleted 1 issue(s)\n")
}
w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// Verify output contains expected elements
if !strings.Contains(output, "Deleted") {
t.Errorf("Expected output to contain 'Deleted', got: %s", output)
}
_ = resultJSON // Suppress unused variable warning
}
// TestDeleteViaDaemon_DependencyConflict tests error handling for dependency conflicts
func TestDeleteViaDaemon_DependencyConflict(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
ctx, cancel, client, testStore, cleanup := setupDaemonTestEnvForDelete(t)
defer cleanup()
defer cancel()
// Create issues with dependencies
blockerIssue := &types.Issue{
Title: "Blocker Issue",
IssueType: "task",
Status: types.StatusOpen,
Priority: 1,
}
if err := testStore.CreateIssue(ctx, blockerIssue, "test"); err != nil {
t.Fatalf("Failed to create blocker issue: %v", err)
}
blockedIssue := &types.Issue{
Title: "Blocked Issue",
IssueType: "task",
Status: types.StatusOpen,
Priority: 2,
}
if err := testStore.CreateIssue(ctx, blockedIssue, "test"); err != nil {
t.Fatalf("Failed to create blocked issue: %v", err)
}
// Create dependency: blockedIssue depends on blockerIssue
dep := &types.Dependency{
IssueID: blockedIssue.ID,
DependsOnID: blockerIssue.ID,
Type: types.DepBlocks,
}
if err := testStore.AddDependency(ctx, dep, "test"); err != nil {
t.Fatalf("Failed to add dependency: %v", err)
}
// Try to delete without force (should fail due to dependency)
deleteArgs := &rpc.DeleteArgs{
IDs: []string{blockerIssue.ID},
Force: false, // No force - should respect dependencies
Cascade: false,
DryRun: true, // Use dry-run to check without modifying
Reason: "dependency conflict test",
}
resp, err := client.Delete(deleteArgs)
if err != nil {
// Error is acceptable for dependency conflicts
t.Logf("Got expected error for dependency conflict: %v", err)
return
}
// Check if response indicates the dependency issue
if resp != nil {
var result map[string]interface{}
if err := json.Unmarshal(resp.Data, &result); err == nil {
t.Logf("Dependency check response: %+v", result)
}
}
}
// TestDeleteViaDaemon_EmptyIDs tests error handling for empty issue ID list
func TestDeleteViaDaemon_EmptyIDs(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
_, cancel, client, _, cleanup := setupDaemonTestEnvForDelete(t)
defer cleanup()
defer cancel()
// Try to delete with empty ID list
deleteArgs := &rpc.DeleteArgs{
IDs: []string{},
Force: true,
DryRun: false,
Reason: "empty ids test",
}
resp, err := client.Delete(deleteArgs)
// Either error or failure response is acceptable
if err == nil && resp != nil && resp.Success {
var result map[string]interface{}
if err := json.Unmarshal(resp.Data, &result); err == nil {
if deletedCount, ok := result["deleted_count"].(float64); ok && deletedCount > 0 {
t.Error("Should not delete anything with empty ID list")
}
}
}
t.Logf("Empty IDs handled: err=%v", err)
}
// TestDeleteViaDaemon_MultipleErrors tests handling of multiple errors in batch deletion
func TestDeleteViaDaemon_MultipleErrors(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
ctx, cancel, client, testStore, cleanup := setupDaemonTestEnvForDelete(t)
defer cleanup()
defer cancel()
// Create one valid issue
validIssue := &types.Issue{
Title: "Valid Issue",
IssueType: "task",
Status: types.StatusOpen,
Priority: 2,
}
if err := testStore.CreateIssue(ctx, validIssue, "test"); err != nil {
t.Fatalf("Failed to create valid issue: %v", err)
}
// Try batch delete with mix of valid and invalid IDs
deleteArgs := &rpc.DeleteArgs{
IDs: []string{validIssue.ID, "test-invalid-1", "test-invalid-2"},
Force: true,
DryRun: false,
Reason: "multiple errors test",
}
resp, err := client.Delete(deleteArgs)
if err != nil {
t.Logf("Got error for mixed batch: %v", err)
return
}
if resp != nil {
var result map[string]interface{}
if err := json.Unmarshal(resp.Data, &result); err == nil {
// Check for errors array
if errors, ok := result["errors"].([]interface{}); ok {
t.Logf("Got %d errors in batch response", len(errors))
}
// Check deleted count
if deletedCount, ok := result["deleted_count"].(float64); ok {
t.Logf("Deleted %d issues despite errors", int(deletedCount))
}
}
}
}
// TestDeleteViaDaemon_DirectCall tests the deleteViaDaemon function directly
// by setting up the global daemonClient
func TestDeleteViaDaemon_DirectCall(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
ctx, cancel, client, testStore, cleanup := setupDaemonTestEnvForDelete(t)
defer cleanup()
defer cancel()
// Create a test issue
issue := &types.Issue{
Title: "Direct Call Test Issue",
IssueType: "task",
Status: types.StatusOpen,
Priority: 2,
}
if err := testStore.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create test issue: %v", err)
}
// Save old global state
oldDaemonClient := daemonClient
oldJsonOutput := jsonOutput
defer func() {
daemonClient = oldDaemonClient
jsonOutput = oldJsonOutput
}()
// Set up global client
daemonClient = client
jsonOutput = true // Use JSON to avoid color codes
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Call deleteViaDaemon directly (should not exit since it succeeds)
deleteViaDaemon([]string{issue.ID}, true, false, false, true, "direct test")
// Restore stdout
w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// Verify output is valid JSON
var result map[string]interface{}
if err := json.Unmarshal([]byte(output), &result); err != nil {
t.Errorf("Expected valid JSON output, got: %s, error: %v", output, err)
}
// Check deleted_count
if deletedCount, ok := result["deleted_count"].(float64); !ok || deletedCount != 1 {
t.Errorf("Expected deleted_count=1, got: %v", result["deleted_count"])
}
}
// TestDeleteViaDaemon_DirectDryRun tests dry-run mode directly
func TestDeleteViaDaemon_DirectDryRun(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
ctx, cancel, client, testStore, cleanup := setupDaemonTestEnvForDelete(t)
defer cleanup()
defer cancel()
// Create a test issue
issue := &types.Issue{
Title: "Direct Dry Run Test Issue",
IssueType: "task",
Status: types.StatusOpen,
Priority: 2,
}
if err := testStore.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create test issue: %v", err)
}
// Save old global state
oldDaemonClient := daemonClient
oldJsonOutput := jsonOutput
defer func() {
daemonClient = oldDaemonClient
jsonOutput = oldJsonOutput
}()
// Set up global client
daemonClient = client
jsonOutput = false // Test human-readable output
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Call deleteViaDaemon with dry-run
deleteViaDaemon([]string{issue.ID}, true, true, false, false, "dry run test")
// Restore stdout
w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// Verify dry-run output
if !strings.Contains(output, "Dry run") && !strings.Contains(output, "would delete") {
t.Logf("Dry run output: %s", output)
}
// Verify issue still exists
existingIssue, err := testStore.GetIssue(ctx, issue.ID)
if err != nil {
t.Fatalf("GetIssue failed: %v", err)
}
if existingIssue == nil {
t.Error("Issue should still exist after dry-run")
}
}
// TestDeleteViaDaemon_DirectHumanOutput tests human-readable output directly
func TestDeleteViaDaemon_DirectHumanOutput(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
ctx, cancel, client, testStore, cleanup := setupDaemonTestEnvForDelete(t)
defer cleanup()
defer cancel()
// Create a test issue
issue := &types.Issue{
Title: "Human Output Test Issue",
IssueType: "task",
Status: types.StatusOpen,
Priority: 2,
}
if err := testStore.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create test issue: %v", err)
}
// Save old global state
oldDaemonClient := daemonClient
oldJsonOutput := jsonOutput
defer func() {
daemonClient = oldDaemonClient
jsonOutput = oldJsonOutput
}()
// Set up global client
daemonClient = client
jsonOutput = false // Human-readable output
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Call deleteViaDaemon with human output
deleteViaDaemon([]string{issue.ID}, true, false, false, false, "human output test")
// Restore stdout
w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// Verify human-readable output contains expected text
if !strings.Contains(output, "Deleted") {
t.Errorf("Expected output to contain 'Deleted', got: %s", output)
}
}
// TestDeleteViaDaemon_DirectBatch tests batch deletion directly
func TestDeleteViaDaemon_DirectBatch(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
ctx, cancel, client, testStore, cleanup := setupDaemonTestEnvForDelete(t)
defer cleanup()
defer cancel()
// Create multiple test issues
var issueIDs []string
for i := 0; i < 3; i++ {
issue := &types.Issue{
Title: "Batch Direct Issue " + string(rune('A'+i)),
IssueType: "task",
Status: types.StatusOpen,
Priority: 2,
}
if err := testStore.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create issue %d: %v", i, err)
}
issueIDs = append(issueIDs, issue.ID)
}
// Save old global state
oldDaemonClient := daemonClient
oldJsonOutput := jsonOutput
defer func() {
daemonClient = oldDaemonClient
jsonOutput = oldJsonOutput
}()
// Set up global client
daemonClient = client
jsonOutput = true
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Call deleteViaDaemon with multiple issues
deleteViaDaemon(issueIDs, true, false, false, true, "batch direct test")
// Restore stdout
w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// Verify output is valid JSON
var result map[string]interface{}
if err := json.Unmarshal([]byte(output), &result); err != nil {
t.Errorf("Expected valid JSON output, got: %s, error: %v", output, err)
}
// Check deleted_count
if deletedCount, ok := result["deleted_count"].(float64); !ok || int(deletedCount) != 3 {
t.Errorf("Expected deleted_count=3, got: %v", result["deleted_count"])
}
}
// setupDaemonTestEnvForDelete sets up a complete daemon test environment
func setupDaemonTestEnvForDelete(t *testing.T) (context.Context, context.CancelFunc, *rpc.Client, *sqlite.SQLiteStorage, func()) {
t.Helper()
tmpDir := makeSocketTempDir(t)
initTestGitRepo(t, tmpDir)
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatalf("Failed to create beads dir: %v", err)
}
socketPath := filepath.Join(beadsDir, "bd.sock")
testDBPath := filepath.Join(beadsDir, "beads.db")
testStore := newTestStore(t, testDBPath)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
log := daemonLogger{logger: slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelInfo}))}
server, _, err := startRPCServer(ctx, socketPath, testStore, tmpDir, testDBPath, log)
if err != nil {
cancel()
t.Fatalf("Failed to start RPC server: %v", err)
}
// Wait for server to be ready
select {
case <-server.WaitReady():
// Server is ready
case <-time.After(5 * time.Second):
cancel()
t.Fatal("Server did not become ready")
}
// Connect RPC client
client, err := rpc.TryConnect(socketPath)
if err != nil || client == nil {
cancel()
t.Fatalf("Failed to connect RPC client: %v", err)
}
cleanup := func() {
if client != nil {
client.Close()
}
if server != nil {
server.Stop()
}
testStore.Close()
}
return ctx, cancel, client, testStore, cleanup
}