Fix bd-8kde: disable auto-import during delete operations
- Skip auto-import in PersistentPreRun for delete command - Prevents resurrection of deleted issues from stale git JSONL - Add TestBulkDeleteNoResurrection to prevent regression - Verified with full test suite Amp-Thread-ID: https://ampcode.com/threads/T-635e5cf4-fa2f-4ce3-9f05-43b48010baae Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
func TestReadIssueIDsFromFile(t *testing.T) {
|
||||
@@ -97,3 +105,167 @@ func TestUniqueStrings(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBulkDeleteNoResurrection(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
testDB := filepath.Join(beadsDir, "beads.db")
|
||||
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
|
||||
testGitInit(t, tmpDir)
|
||||
|
||||
s := newTestStore(t, testDB)
|
||||
ctx := context.Background()
|
||||
|
||||
totalIssues := 20
|
||||
toDeleteCount := 10
|
||||
var toDelete []string
|
||||
|
||||
for i := 1; i <= totalIssues; i++ {
|
||||
issue := &types.Issue{
|
||||
Title: "Issue " + string(rune('A'+i-1)),
|
||||
Description: "Test issue",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: "task",
|
||||
}
|
||||
if err := s.CreateIssue(ctx, issue, "test"); err != nil {
|
||||
t.Fatalf("Failed to create issue %d: %v", i, err)
|
||||
}
|
||||
if i <= toDeleteCount {
|
||||
toDelete = append(toDelete, issue.ID)
|
||||
}
|
||||
}
|
||||
|
||||
exportToJSONLTest(t, s, jsonlPath)
|
||||
testGitCommit(t, tmpDir, jsonlPath, "Add issues")
|
||||
|
||||
oldStore := store
|
||||
oldDbPath := dbPath
|
||||
oldAutoImportEnabled := autoImportEnabled
|
||||
defer func() {
|
||||
store = oldStore
|
||||
dbPath = oldDbPath
|
||||
autoImportEnabled = oldAutoImportEnabled
|
||||
}()
|
||||
|
||||
store = s
|
||||
dbPath = testDB
|
||||
autoImportEnabled = true
|
||||
|
||||
result, err := s.DeleteIssues(ctx, toDelete, false, true, false)
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteIssues failed: %v", err)
|
||||
}
|
||||
|
||||
if result.DeletedCount != toDeleteCount {
|
||||
t.Errorf("Expected %d deletions, got %d", toDeleteCount, result.DeletedCount)
|
||||
}
|
||||
|
||||
for _, id := range toDelete {
|
||||
if err := removeIssueFromJSONL(id); err != nil {
|
||||
t.Fatalf("removeIssueFromJSONL failed for %s: %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
stats, err := s.GetStatistics(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetStatistics failed: %v", err)
|
||||
}
|
||||
|
||||
expectedRemaining := totalIssues - toDeleteCount
|
||||
if stats.TotalIssues != expectedRemaining {
|
||||
t.Errorf("After delete: expected %d issues in DB, got %d", expectedRemaining, stats.TotalIssues)
|
||||
}
|
||||
|
||||
jsonlIssues := countJSONLIssuesTest(t, jsonlPath)
|
||||
if jsonlIssues != expectedRemaining {
|
||||
t.Errorf("After delete: expected %d issues in JSONL, got %d", expectedRemaining, jsonlIssues)
|
||||
}
|
||||
|
||||
for _, id := range toDelete {
|
||||
issue, err := s.GetIssue(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssue failed for %s: %v", id, err)
|
||||
}
|
||||
if issue != nil {
|
||||
t.Errorf("Deleted issue %s was resurrected!", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func exportToJSONLTest(t *testing.T, s *sqlite.SQLiteStorage, jsonlPath string) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
issues, err := s.SearchIssues(ctx, "", types.IssueFilter{})
|
||||
if err != nil {
|
||||
t.Fatalf("SearchIssues failed: %v", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(jsonlPath), 0755); err != nil {
|
||||
t.Fatalf("Failed to create JSONL dir: %v", err)
|
||||
}
|
||||
|
||||
f, err := os.Create(jsonlPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create JSONL: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
enc := json.NewEncoder(f)
|
||||
for _, iss := range issues {
|
||||
if err := enc.Encode(iss); err != nil {
|
||||
t.Fatalf("Failed to encode issue: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testGitInit(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
testGitCmd(t, dir, "init")
|
||||
testGitCmd(t, dir, "config", "user.email", "test@example.com")
|
||||
testGitCmd(t, dir, "config", "user.name", "Test User")
|
||||
}
|
||||
|
||||
func testGitCommit(t *testing.T, dir, file, msg string) {
|
||||
t.Helper()
|
||||
testGitCmd(t, dir, "add", file)
|
||||
testGitCmd(t, dir, "commit", "-m", msg)
|
||||
}
|
||||
|
||||
func testGitCmd(t *testing.T, dir string, args ...string) {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = dir
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("git %v failed: %v\nOutput: %s", args, err, output)
|
||||
}
|
||||
}
|
||||
|
||||
func countJSONLIssuesTest(t *testing.T, jsonlPath string) int {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(jsonlPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0
|
||||
}
|
||||
t.Fatalf("Failed to read JSONL: %v", err)
|
||||
}
|
||||
|
||||
count := 0
|
||||
scanner := bufio.NewScanner(bytes.NewReader(data))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(bytes.TrimSpace([]byte(line))) > 0 {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
t.Fatalf("Scanner error: %v", err)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -415,8 +415,9 @@ var rootCmd = &cobra.Command{
|
||||
|
||||
// Auto-import if JSONL is newer than DB (e.g., after git pull)
|
||||
// Skip for import command itself to avoid recursion
|
||||
// Skip for delete command to prevent resurrection of deleted issues (bd-8kde)
|
||||
// Skip if sync --dry-run to avoid modifying DB in dry-run mode (bd-191)
|
||||
if cmd.Name() != "import" && autoImportEnabled {
|
||||
if cmd.Name() != "import" && cmd.Name() != "delete" && autoImportEnabled {
|
||||
// Check if this is sync command with --dry-run flag
|
||||
if cmd.Name() == "sync" {
|
||||
if dryRun, _ := cmd.Flags().GetBool("dry-run"); dryRun {
|
||||
|
||||
Reference in New Issue
Block a user