feat(reset): add integration tests and doctor reset suggestion
- Add 9 integration tests for bd reset command (reset_test.go) - Tests cover: dry-run, force, skip-init, backup, confirmation, cancellation, no-beads-dir, multiple issues, verbose mode - Enhance bd doctor to suggest reset when >= 3 unfixable errors found - Addresses bd-aydr.7, bd-aydr.5, bd-aydr.8 Closes: #479 (via GitHub comment with solution) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1318,6 +1318,7 @@ func printDiagnostics(result doctorResult) {
|
||||
|
||||
// Print warnings/errors with fixes
|
||||
hasIssues := false
|
||||
unfixableErrors := 0
|
||||
for _, check := range result.Checks {
|
||||
if check.Status != statusOK && check.Fix != "" {
|
||||
if !hasIssues {
|
||||
@@ -1332,12 +1333,27 @@ func printDiagnostics(result doctorResult) {
|
||||
}
|
||||
|
||||
fmt.Printf(" Fix: %s\n\n", check.Fix)
|
||||
} else if check.Status == statusError && check.Fix == "" {
|
||||
// Count unfixable errors
|
||||
unfixableErrors++
|
||||
}
|
||||
}
|
||||
|
||||
if !hasIssues {
|
||||
color.Green("✓ All checks passed\n")
|
||||
}
|
||||
|
||||
// Suggest reset if there are multiple unfixable errors
|
||||
if unfixableErrors >= 3 {
|
||||
fmt.Println()
|
||||
color.Yellow("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
|
||||
color.Yellow("⚠ Found %d unfixable errors\n", unfixableErrors)
|
||||
fmt.Println()
|
||||
fmt.Println(" Your beads state may be too corrupted to repair automatically.")
|
||||
fmt.Println(" Consider running 'bd reset' to start fresh.")
|
||||
fmt.Println(" (Use 'bd reset --backup' to save current state first)")
|
||||
color.Yellow("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
|
||||
}
|
||||
}
|
||||
|
||||
func checkMultipleDatabases(path string) doctorCheck {
|
||||
|
||||
331
cmd/bd/reset_test.go
Normal file
331
cmd/bd/reset_test.go
Normal file
@@ -0,0 +1,331 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// runBDResetExec runs bd reset via exec.Command for clean state isolation
|
||||
// Reset has persistent flag state that doesn't work well with in-process testing
|
||||
func runBDResetExec(t *testing.T, dir string, stdin string, args ...string) (string, error) {
|
||||
t.Helper()
|
||||
|
||||
// Add --no-daemon to all commands
|
||||
args = append([]string{"--no-daemon"}, args...)
|
||||
|
||||
cmd := exec.Command(testBD, args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(), "BEADS_NO_DAEMON=1")
|
||||
|
||||
if stdin != "" {
|
||||
cmd.Stdin = strings.NewReader(stdin)
|
||||
}
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
func TestCLI_ResetDryRun(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping slow CLI test in short mode")
|
||||
}
|
||||
tmpDir := setupCLITestDB(t)
|
||||
|
||||
// Create some issues
|
||||
runBDExec(t, tmpDir, "create", "Issue 1", "-p", "1")
|
||||
runBDExec(t, tmpDir, "create", "Issue 2", "-p", "2")
|
||||
|
||||
// Run dry-run reset
|
||||
out, err := runBDResetExec(t, tmpDir, "", "reset", "--dry-run")
|
||||
if err != nil {
|
||||
t.Fatalf("dry-run reset failed: %v\nOutput: %s", err, out)
|
||||
}
|
||||
|
||||
// Verify output contains impact summary
|
||||
if !strings.Contains(out, "Reset Impact Summary") {
|
||||
t.Errorf("Expected 'Reset Impact Summary' in output, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "dry run") {
|
||||
t.Errorf("Expected 'dry run' in output, got: %s", out)
|
||||
}
|
||||
|
||||
// Verify .beads directory still exists (dry run shouldn't delete anything)
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||
t.Error("dry run should not delete .beads directory")
|
||||
}
|
||||
|
||||
// Verify we can still list issues
|
||||
listOut := runBDExec(t, tmpDir, "list")
|
||||
if !strings.Contains(listOut, "Issue 1") {
|
||||
t.Errorf("Issues should still exist after dry run, got: %s", listOut)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLI_ResetForce(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping slow CLI test in short mode")
|
||||
}
|
||||
tmpDir := setupCLITestDB(t)
|
||||
|
||||
// Create some issues
|
||||
runBDExec(t, tmpDir, "create", "Issue to delete", "-p", "1")
|
||||
|
||||
// Run reset with --force (no confirmation needed)
|
||||
out, err := runBDResetExec(t, tmpDir, "", "reset", "--force")
|
||||
if err != nil {
|
||||
t.Fatalf("reset --force failed: %v\nOutput: %s", err, out)
|
||||
}
|
||||
|
||||
// Verify success message
|
||||
if !strings.Contains(out, "Reset complete") {
|
||||
t.Errorf("Expected 'Reset complete' in output, got: %s", out)
|
||||
}
|
||||
|
||||
// Verify .beads directory was recreated (reinit by default)
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||
t.Error(".beads directory should be recreated after reset")
|
||||
}
|
||||
|
||||
// Verify issues are gone (reinit creates empty workspace)
|
||||
listOut := runBDExec(t, tmpDir, "list")
|
||||
if strings.Contains(listOut, "Issue to delete") {
|
||||
t.Errorf("Issues should be deleted after reset, got: %s", listOut)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLI_ResetSkipInit(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping slow CLI test in short mode")
|
||||
}
|
||||
tmpDir := setupCLITestDB(t)
|
||||
|
||||
// Create an issue
|
||||
runBDExec(t, tmpDir, "create", "Test issue", "-p", "1")
|
||||
|
||||
// Run reset with --skip-init
|
||||
out, err := runBDResetExec(t, tmpDir, "", "reset", "--force", "--skip-init")
|
||||
if err != nil {
|
||||
t.Fatalf("reset --skip-init failed: %v\nOutput: %s", err, out)
|
||||
}
|
||||
|
||||
// Verify .beads directory doesn't exist
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if _, err := os.Stat(beadsDir); !os.IsNotExist(err) {
|
||||
t.Error(".beads directory should not exist after reset with --skip-init")
|
||||
}
|
||||
|
||||
// Verify output mentions bd init
|
||||
if !strings.Contains(out, "bd init") {
|
||||
t.Errorf("Expected hint about 'bd init' in output, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLI_ResetBackup(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping slow CLI test in short mode")
|
||||
}
|
||||
tmpDir := setupCLITestDB(t)
|
||||
|
||||
// Create some issues
|
||||
runBDExec(t, tmpDir, "create", "Backup test issue", "-p", "1")
|
||||
|
||||
// Run reset with --backup
|
||||
out, err := runBDResetExec(t, tmpDir, "", "reset", "--force", "--backup")
|
||||
if err != nil {
|
||||
t.Fatalf("reset --backup failed: %v\nOutput: %s", err, out)
|
||||
}
|
||||
|
||||
// Verify backup was mentioned in output
|
||||
if !strings.Contains(out, "Backup created") || !strings.Contains(out, ".beads-backup-") {
|
||||
t.Errorf("Expected backup path in output, got: %s", out)
|
||||
}
|
||||
|
||||
// Verify a backup directory exists
|
||||
entries, err := os.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read dir: %v", err)
|
||||
}
|
||||
|
||||
foundBackup := false
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.Name(), ".beads-backup-") && entry.IsDir() {
|
||||
foundBackup = true
|
||||
// Verify backup has content
|
||||
backupPath := filepath.Join(tmpDir, entry.Name())
|
||||
backupEntries, err := os.ReadDir(backupPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read backup dir: %v", err)
|
||||
}
|
||||
if len(backupEntries) == 0 {
|
||||
t.Error("Backup directory should not be empty")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundBackup {
|
||||
t.Error("No backup directory found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLI_ResetWithConfirmation(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping slow CLI test in short mode")
|
||||
}
|
||||
tmpDir := setupCLITestDB(t)
|
||||
|
||||
// Create an issue
|
||||
runBDExec(t, tmpDir, "create", "Confirm test", "-p", "1")
|
||||
|
||||
// Run reset with confirmation (type 'y')
|
||||
out, err := runBDResetExec(t, tmpDir, "y\n", "reset")
|
||||
if err != nil {
|
||||
t.Fatalf("reset with confirmation failed: %v\nOutput: %s", err, out)
|
||||
}
|
||||
|
||||
// Verify success
|
||||
if !strings.Contains(out, "Reset complete") {
|
||||
t.Errorf("Expected 'Reset complete' in output, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLI_ResetCancelled(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping slow CLI test in short mode")
|
||||
}
|
||||
tmpDir := setupCLITestDB(t)
|
||||
|
||||
// Create an issue
|
||||
runBDExec(t, tmpDir, "create", "Keep this", "-p", "1")
|
||||
|
||||
// Run reset but cancel (type 'n')
|
||||
out, err := runBDResetExec(t, tmpDir, "n\n", "reset")
|
||||
// Cancellation is not an error
|
||||
if err != nil {
|
||||
t.Fatalf("reset cancellation failed: %v\nOutput: %s", err, out)
|
||||
}
|
||||
|
||||
// Verify cancelled message
|
||||
if !strings.Contains(out, "cancelled") {
|
||||
t.Errorf("Expected 'cancelled' in output, got: %s", out)
|
||||
}
|
||||
|
||||
// Verify issue still exists
|
||||
listOut := runBDExec(t, tmpDir, "list")
|
||||
if !strings.Contains(listOut, "Keep this") {
|
||||
t.Errorf("Issues should still exist after cancelled reset, got: %s", listOut)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLI_ResetNoBeadsDir(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping slow CLI test in short mode")
|
||||
}
|
||||
// Create temp dir without .beads
|
||||
tmpDir := createTempDirWithCleanup(t)
|
||||
|
||||
// Run reset - should fail
|
||||
out, err := runBDResetExec(t, tmpDir, "", "reset", "--force")
|
||||
if err == nil {
|
||||
t.Error("reset should fail when no .beads directory exists")
|
||||
}
|
||||
|
||||
// Verify error message
|
||||
if !strings.Contains(out, "no .beads directory found") && !strings.Contains(out, "Error") {
|
||||
t.Errorf("Expected error about missing .beads directory, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLI_ResetWithIssues(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping slow CLI test in short mode")
|
||||
}
|
||||
tmpDir := setupCLITestDB(t)
|
||||
|
||||
// Create multiple issues with different states
|
||||
runBDExec(t, tmpDir, "create", "Open issue 1", "-p", "1")
|
||||
runBDExec(t, tmpDir, "create", "Open issue 2", "-p", "2")
|
||||
|
||||
out1 := runBDExec(t, tmpDir, "create", "To close", "-p", "1", "--json")
|
||||
id := extractIDFromJSON(t, out1)
|
||||
runBDExec(t, tmpDir, "close", id)
|
||||
|
||||
// Run dry-run to see counts
|
||||
out, err := runBDResetExec(t, tmpDir, "", "reset", "--dry-run")
|
||||
if err != nil {
|
||||
t.Fatalf("dry-run failed: %v\nOutput: %s", err, out)
|
||||
}
|
||||
|
||||
// Verify impact shows correct counts
|
||||
if !strings.Contains(out, "Issues to delete") {
|
||||
t.Errorf("Expected 'Issues to delete' in output, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "Open:") {
|
||||
t.Errorf("Expected 'Open:' count in output, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "Closed:") {
|
||||
t.Errorf("Expected 'Closed:' count in output, got: %s", out)
|
||||
}
|
||||
|
||||
// Now do actual reset
|
||||
out, err = runBDResetExec(t, tmpDir, "", "reset", "--force")
|
||||
if err != nil {
|
||||
t.Fatalf("reset failed: %v\nOutput: %s", err, out)
|
||||
}
|
||||
|
||||
// Verify all issues are gone
|
||||
listOut := runBDExec(t, tmpDir, "list")
|
||||
if strings.Contains(listOut, "Open issue") || strings.Contains(listOut, "To close") {
|
||||
t.Errorf("All issues should be deleted after reset, got: %s", listOut)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLI_ResetVerbose(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping slow CLI test in short mode")
|
||||
}
|
||||
tmpDir := setupCLITestDB(t)
|
||||
|
||||
// Create an issue
|
||||
runBDExec(t, tmpDir, "create", "Verbose test", "-p", "1")
|
||||
|
||||
// Run reset with --verbose
|
||||
out, err := runBDResetExec(t, tmpDir, "", "reset", "--force", "--verbose")
|
||||
if err != nil {
|
||||
t.Fatalf("reset --verbose failed: %v\nOutput: %s", err, out)
|
||||
}
|
||||
|
||||
// Verify verbose output shows more details
|
||||
if !strings.Contains(out, "Starting reset") {
|
||||
t.Errorf("Expected 'Starting reset' in verbose output, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// extractIDFromJSON extracts an ID from JSON output
|
||||
func extractIDFromJSON(t *testing.T, out string) string {
|
||||
t.Helper()
|
||||
// Try both formats: "id":"xxx" and "id": "xxx"
|
||||
idx := strings.Index(out, `"id":"`)
|
||||
offset := 6
|
||||
if idx == -1 {
|
||||
idx = strings.Index(out, `"id": "`)
|
||||
offset = 7
|
||||
}
|
||||
if idx == -1 {
|
||||
t.Fatalf("No id found in JSON output: %s", out)
|
||||
}
|
||||
start := idx + offset
|
||||
end := strings.Index(out[start:], `"`)
|
||||
if end == -1 {
|
||||
t.Fatalf("Malformed JSON output: %s", out)
|
||||
}
|
||||
return out[start : start+end]
|
||||
}
|
||||
Reference in New Issue
Block a user