feat: Add bd repair command for orphaned foreign key refs (hq-2cchm)
When the database has orphaned dependencies or labels, the migration invariant check fails and prevents the database from opening. This creates a chicken-and-egg problem where bd doctor --fix cannot run. The new bd repair command: - Opens SQLite directly, bypassing invariant checks - Deletes orphaned dependencies (issue_id or depends_on_id not in issues) - Deletes orphaned labels (issue_id not in issues) - Runs WAL checkpoint to persist changes - Supports --dry-run to preview what would be cleaned 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -281,6 +281,7 @@ var rootCmd = &cobra.Command{
|
||||
"powershell",
|
||||
"prime",
|
||||
"quickstart",
|
||||
"repair",
|
||||
"setup",
|
||||
"version",
|
||||
"zsh",
|
||||
|
||||
307
cmd/bd/repair.go
Normal file
307
cmd/bd/repair.go
Normal file
@@ -0,0 +1,307 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
|
||||
var repairCmd = &cobra.Command{
|
||||
Use: "repair",
|
||||
GroupID: GroupMaintenance,
|
||||
Short: "Repair corrupted database by cleaning orphaned references",
|
||||
// PreRun disables PersistentPreRun for this command (we open DB directly, bypassing invariants)
|
||||
PreRun: func(cmd *cobra.Command, args []string) {},
|
||||
Long: `Repair a database that won't open due to orphaned foreign key references.
|
||||
|
||||
When the database has orphaned dependencies or labels, the migration invariant
|
||||
check fails and prevents the database from opening. This creates a chicken-and-egg
|
||||
problem where 'bd doctor --fix' can't run because it can't open the database.
|
||||
|
||||
This command opens SQLite directly (bypassing invariant checks) and cleans:
|
||||
- Orphaned dependencies (issue_id not in issues)
|
||||
- Orphaned dependencies (depends_on_id not in issues, excluding external refs)
|
||||
- Orphaned labels (issue_id not in issues)
|
||||
|
||||
After repair, normal bd commands should work again.
|
||||
|
||||
Examples:
|
||||
bd repair # Repair database in current directory
|
||||
bd repair --dry-run # Show what would be cleaned without making changes
|
||||
bd repair --path /other/repo # Repair database in another location`,
|
||||
Run: runRepair,
|
||||
}
|
||||
|
||||
var (
|
||||
repairDryRun bool
|
||||
repairPath string
|
||||
)
|
||||
|
||||
func init() {
|
||||
repairCmd.Flags().BoolVar(&repairDryRun, "dry-run", false, "Show what would be cleaned without making changes")
|
||||
repairCmd.Flags().StringVar(&repairPath, "path", ".", "Path to repository with .beads directory")
|
||||
rootCmd.AddCommand(repairCmd)
|
||||
}
|
||||
|
||||
func runRepair(cmd *cobra.Command, args []string) {
|
||||
// Find .beads directory
|
||||
beadsDir := filepath.Join(repairPath, ".beads")
|
||||
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "Error: .beads directory not found at %s\n", beadsDir)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Find database file
|
||||
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "Error: database not found at %s\n", dbPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Repairing database: %s\n", dbPath)
|
||||
if repairDryRun {
|
||||
fmt.Println("[DRY-RUN] No changes will be made")
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// Open database directly, bypassing beads storage layer
|
||||
db, err := openRepairDB(dbPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error opening database: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Collect repair statistics
|
||||
stats := repairStats{}
|
||||
|
||||
// 1. Find and clean orphaned dependencies (issue_id not in issues)
|
||||
orphanedIssueID, err := findOrphanedDepsIssueID(db)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error checking orphaned deps (issue_id): %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
stats.orphanedDepsIssueID = len(orphanedIssueID)
|
||||
|
||||
// 2. Find and clean orphaned dependencies (depends_on_id not in issues)
|
||||
orphanedDependsOn, err := findOrphanedDepsDependsOn(db)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error checking orphaned deps (depends_on_id): %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
stats.orphanedDepsDependsOn = len(orphanedDependsOn)
|
||||
|
||||
// 3. Find and clean orphaned labels
|
||||
orphanedLabels, err := findOrphanedLabels(db)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error checking orphaned labels: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
stats.orphanedLabels = len(orphanedLabels)
|
||||
|
||||
// Print findings
|
||||
if stats.total() == 0 {
|
||||
fmt.Printf("%s No orphaned references found - database is clean\n", ui.RenderPass("✓"))
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d orphaned reference(s):\n", stats.total())
|
||||
if stats.orphanedDepsIssueID > 0 {
|
||||
fmt.Printf(" • %d dependencies with missing issue_id\n", stats.orphanedDepsIssueID)
|
||||
for _, dep := range orphanedIssueID {
|
||||
fmt.Printf(" - %s → %s\n", dep.issueID, dep.dependsOnID)
|
||||
}
|
||||
}
|
||||
if stats.orphanedDepsDependsOn > 0 {
|
||||
fmt.Printf(" • %d dependencies with missing depends_on_id\n", stats.orphanedDepsDependsOn)
|
||||
for _, dep := range orphanedDependsOn {
|
||||
fmt.Printf(" - %s → %s\n", dep.issueID, dep.dependsOnID)
|
||||
}
|
||||
}
|
||||
if stats.orphanedLabels > 0 {
|
||||
fmt.Printf(" • %d labels with missing issue_id\n", stats.orphanedLabels)
|
||||
for _, label := range orphanedLabels {
|
||||
fmt.Printf(" - %s: %s\n", label.issueID, label.label)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
if repairDryRun {
|
||||
fmt.Printf("[DRY-RUN] Would delete %d orphaned reference(s)\n", stats.total())
|
||||
return
|
||||
}
|
||||
|
||||
// Apply repairs
|
||||
fmt.Println("Cleaning orphaned references...")
|
||||
|
||||
// Delete orphaned deps (issue_id)
|
||||
if len(orphanedIssueID) > 0 {
|
||||
result, err := db.Exec(`
|
||||
DELETE FROM dependencies
|
||||
WHERE NOT EXISTS (SELECT 1 FROM issues WHERE id = dependencies.issue_id)
|
||||
`)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error deleting orphaned deps (issue_id): %v\n", err)
|
||||
} else {
|
||||
deleted, _ := result.RowsAffected()
|
||||
fmt.Printf(" %s Deleted %d dependencies with missing issue_id\n", ui.RenderPass("✓"), deleted)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete orphaned deps (depends_on_id)
|
||||
if len(orphanedDependsOn) > 0 {
|
||||
result, err := db.Exec(`
|
||||
DELETE FROM dependencies
|
||||
WHERE NOT EXISTS (SELECT 1 FROM issues WHERE id = dependencies.depends_on_id)
|
||||
AND dependencies.depends_on_id NOT LIKE 'external:%'
|
||||
`)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error deleting orphaned deps (depends_on_id): %v\n", err)
|
||||
} else {
|
||||
deleted, _ := result.RowsAffected()
|
||||
fmt.Printf(" %s Deleted %d dependencies with missing depends_on_id\n", ui.RenderPass("✓"), deleted)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete orphaned labels
|
||||
if len(orphanedLabels) > 0 {
|
||||
result, err := db.Exec(`
|
||||
DELETE FROM labels
|
||||
WHERE NOT EXISTS (SELECT 1 FROM issues WHERE id = labels.issue_id)
|
||||
`)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error deleting orphaned labels: %v\n", err)
|
||||
} else {
|
||||
deleted, _ := result.RowsAffected()
|
||||
fmt.Printf(" %s Deleted %d labels with missing issue_id\n", ui.RenderPass("✓"), deleted)
|
||||
}
|
||||
}
|
||||
|
||||
// Run WAL checkpoint to persist changes
|
||||
fmt.Print(" Running WAL checkpoint... ")
|
||||
if _, err := db.Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil {
|
||||
fmt.Printf("%s %v\n", ui.RenderFail("✗"), err)
|
||||
} else {
|
||||
fmt.Printf("%s\n", ui.RenderPass("✓"))
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("%s Repair complete. Try running 'bd doctor' to verify.\n", ui.RenderPass("✓"))
|
||||
}
|
||||
|
||||
// repairStats tracks what was found/cleaned
|
||||
type repairStats struct {
|
||||
orphanedDepsIssueID int
|
||||
orphanedDepsDependsOn int
|
||||
orphanedLabels int
|
||||
}
|
||||
|
||||
func (s repairStats) total() int {
|
||||
return s.orphanedDepsIssueID + s.orphanedDepsDependsOn + s.orphanedLabels
|
||||
}
|
||||
|
||||
// orphanedDep represents an orphaned dependency
|
||||
type orphanedDep struct {
|
||||
issueID string
|
||||
dependsOnID string
|
||||
}
|
||||
|
||||
// orphanedLabel represents an orphaned label
|
||||
type orphanedLabel struct {
|
||||
issueID string
|
||||
label string
|
||||
}
|
||||
|
||||
// openRepairDB opens SQLite directly for repair, bypassing all beads layer code
|
||||
func openRepairDB(dbPath string) (*sql.DB, error) {
|
||||
// Build connection string with pragmas
|
||||
busyMs := int64(30 * time.Second / time.Millisecond)
|
||||
if v := strings.TrimSpace(os.Getenv("BD_LOCK_TIMEOUT")); v != "" {
|
||||
if d, err := time.ParseDuration(v); err == nil {
|
||||
busyMs = int64(d / time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
connStr := fmt.Sprintf("file:%s?_pragma=busy_timeout(%d)&_pragma=foreign_keys(OFF)&_time_format=sqlite",
|
||||
dbPath, busyMs)
|
||||
|
||||
return sql.Open("sqlite3", connStr)
|
||||
}
|
||||
|
||||
// findOrphanedDepsIssueID finds dependencies where issue_id doesn't exist
|
||||
func findOrphanedDepsIssueID(db *sql.DB) ([]orphanedDep, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT d.issue_id, d.depends_on_id
|
||||
FROM dependencies d
|
||||
WHERE NOT EXISTS (SELECT 1 FROM issues WHERE id = d.issue_id)
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var orphans []orphanedDep
|
||||
for rows.Next() {
|
||||
var dep orphanedDep
|
||||
if err := rows.Scan(&dep.issueID, &dep.dependsOnID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orphans = append(orphans, dep)
|
||||
}
|
||||
return orphans, rows.Err()
|
||||
}
|
||||
|
||||
// findOrphanedDepsDependsOn finds dependencies where depends_on_id doesn't exist
|
||||
func findOrphanedDepsDependsOn(db *sql.DB) ([]orphanedDep, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT d.issue_id, d.depends_on_id
|
||||
FROM dependencies d
|
||||
WHERE NOT EXISTS (SELECT 1 FROM issues WHERE id = d.depends_on_id)
|
||||
AND d.depends_on_id NOT LIKE 'external:%'
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var orphans []orphanedDep
|
||||
for rows.Next() {
|
||||
var dep orphanedDep
|
||||
if err := rows.Scan(&dep.issueID, &dep.dependsOnID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orphans = append(orphans, dep)
|
||||
}
|
||||
return orphans, rows.Err()
|
||||
}
|
||||
|
||||
// findOrphanedLabels finds labels where issue_id doesn't exist
|
||||
func findOrphanedLabels(db *sql.DB) ([]orphanedLabel, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT l.issue_id, l.label
|
||||
FROM labels l
|
||||
WHERE NOT EXISTS (SELECT 1 FROM issues WHERE id = l.issue_id)
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var labels []orphanedLabel
|
||||
for rows.Next() {
|
||||
var label orphanedLabel
|
||||
if err := rows.Scan(&label.issueID, &label.label); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
labels = append(labels, label)
|
||||
}
|
||||
return labels, rows.Err()
|
||||
}
|
||||
201
cmd/bd/repair_orphans_test.go
Normal file
201
cmd/bd/repair_orphans_test.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
)
|
||||
|
||||
// runBDRepair runs the repair command with --path flag (bypasses normal db init)
|
||||
func runBDRepair(t *testing.T, exe, path string, args ...string) (string, error) {
|
||||
t.Helper()
|
||||
fullArgs := []string{"repair", "--path", path}
|
||||
fullArgs = append(fullArgs, args...)
|
||||
|
||||
cmd := exec.Command(exe, fullArgs...)
|
||||
cmd.Dir = path
|
||||
cmd.Env = append(os.Environ(), "BEADS_NO_DAEMON=1")
|
||||
out, err := cmd.CombinedOutput()
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
func TestRepairOrphans_DryRun(t *testing.T) {
|
||||
requireTestGuardDisabled(t)
|
||||
|
||||
if testing.Short() {
|
||||
t.Skip("skipping slow repair test in short mode")
|
||||
}
|
||||
|
||||
bdExe := buildBDForTest(t)
|
||||
ws := mkTmpDirInTmp(t, "bd-repair-orphans-*")
|
||||
dbPath := filepath.Join(ws, ".beads", "beads.db")
|
||||
|
||||
// Initialize with some issues
|
||||
if _, err := runBDSideDB(t, bdExe, ws, dbPath, "init", "--prefix", "test", "--quiet"); err != nil {
|
||||
t.Fatalf("bd init failed: %v", err)
|
||||
}
|
||||
if _, err := runBDSideDB(t, bdExe, ws, dbPath, "create", "Issue 1", "-p", "1"); err != nil {
|
||||
t.Fatalf("bd create failed: %v", err)
|
||||
}
|
||||
|
||||
// Directly insert orphaned data into the database
|
||||
db, err := sql.Open("sqlite3", "file:"+dbPath+"?_pragma=foreign_keys(OFF)")
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Insert orphaned dependency (issue_id doesn't exist)
|
||||
_, err = db.Exec("INSERT INTO dependencies (issue_id, depends_on_id, type, created_by) VALUES ('nonexistent-1', 'test-xyz', 'blocks', 'test')")
|
||||
if err != nil {
|
||||
t.Fatalf("insert orphan dep: %v", err)
|
||||
}
|
||||
|
||||
// Insert orphaned dependency (depends_on_id doesn't exist)
|
||||
_, err = db.Exec("INSERT INTO dependencies (issue_id, depends_on_id, type, created_by) VALUES ('test-xyz', 'deleted-issue', 'blocks', 'test')")
|
||||
if err != nil {
|
||||
t.Fatalf("insert orphan dep2: %v", err)
|
||||
}
|
||||
|
||||
// Insert orphaned label
|
||||
_, err = db.Exec("INSERT INTO labels (issue_id, label) VALUES ('gone-issue', 'bug')")
|
||||
if err != nil {
|
||||
t.Fatalf("insert orphan label: %v", err)
|
||||
}
|
||||
db.Close()
|
||||
|
||||
// Run repair with --dry-run (use --path, not --db, so repair bypasses normal init)
|
||||
out, err := runBDRepair(t, bdExe, ws, "--dry-run")
|
||||
if err != nil {
|
||||
t.Fatalf("bd repair --dry-run failed: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
// Verify it found the orphans
|
||||
if !strings.Contains(out, "dependencies with missing issue_id") {
|
||||
t.Errorf("expected to find orphaned deps (issue_id), got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "dependencies with missing depends_on_id") {
|
||||
t.Errorf("expected to find orphaned deps (depends_on_id), got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "labels with missing issue_id") {
|
||||
t.Errorf("expected to find orphaned labels, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "[DRY-RUN]") {
|
||||
t.Errorf("expected DRY-RUN message, got: %s", out)
|
||||
}
|
||||
|
||||
// Verify data wasn't actually deleted
|
||||
db2, err := sql.Open("sqlite3", "file:"+dbPath+"?_pragma=foreign_keys(OFF)")
|
||||
if err != nil {
|
||||
t.Fatalf("reopen db: %v", err)
|
||||
}
|
||||
defer db2.Close()
|
||||
|
||||
var count int
|
||||
db2.QueryRow("SELECT COUNT(*) FROM dependencies WHERE issue_id = 'nonexistent-1'").Scan(&count)
|
||||
if count != 1 {
|
||||
t.Errorf("expected orphan dep to still exist after dry-run, got count=%d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepairOrphans_Fix(t *testing.T) {
|
||||
requireTestGuardDisabled(t)
|
||||
|
||||
if testing.Short() {
|
||||
t.Skip("skipping slow repair test in short mode")
|
||||
}
|
||||
|
||||
bdExe := buildBDForTest(t)
|
||||
ws := mkTmpDirInTmp(t, "bd-repair-fix-*")
|
||||
dbPath := filepath.Join(ws, ".beads", "beads.db")
|
||||
|
||||
// Initialize with some issues
|
||||
if _, err := runBDSideDB(t, bdExe, ws, dbPath, "init", "--prefix", "test", "--quiet"); err != nil {
|
||||
t.Fatalf("bd init failed: %v", err)
|
||||
}
|
||||
out, err := runBDSideDB(t, bdExe, ws, dbPath, "create", "Issue 1", "-p", "1", "--json")
|
||||
if err != nil {
|
||||
t.Fatalf("bd create failed: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
// Directly insert orphaned data
|
||||
db, err := sql.Open("sqlite3", "file:"+dbPath+"?_pragma=foreign_keys(OFF)")
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
|
||||
// Insert orphaned dependencies and labels
|
||||
db.Exec("INSERT INTO dependencies (issue_id, depends_on_id, type, created_by) VALUES ('orphan-1', 'test-xyz', 'blocks', 'test')")
|
||||
db.Exec("INSERT INTO dependencies (issue_id, depends_on_id, type, created_by) VALUES ('test-xyz', 'deleted-2', 'blocks', 'test')")
|
||||
db.Exec("INSERT INTO labels (issue_id, label) VALUES ('orphan-3', 'wontfix')")
|
||||
db.Close()
|
||||
|
||||
// Run repair (no --dry-run) - use --path to bypass normal db init
|
||||
out, err = runBDRepair(t, bdExe, ws)
|
||||
if err != nil {
|
||||
t.Fatalf("bd repair failed: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
// Verify it cleaned up
|
||||
if !strings.Contains(out, "Deleted") {
|
||||
t.Errorf("expected deletion messages, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "Repair complete") {
|
||||
t.Errorf("expected completion message, got: %s", out)
|
||||
}
|
||||
|
||||
// Verify orphans are gone
|
||||
db2, err := sql.Open("sqlite3", "file:"+dbPath+"?_pragma=foreign_keys(OFF)")
|
||||
if err != nil {
|
||||
t.Fatalf("reopen db: %v", err)
|
||||
}
|
||||
defer db2.Close()
|
||||
|
||||
var depCount, labelCount int
|
||||
db2.QueryRow("SELECT COUNT(*) FROM dependencies WHERE issue_id = 'orphan-1' OR depends_on_id = 'deleted-2'").Scan(&depCount)
|
||||
db2.QueryRow("SELECT COUNT(*) FROM labels WHERE issue_id = 'orphan-3'").Scan(&labelCount)
|
||||
|
||||
if depCount != 0 {
|
||||
t.Errorf("expected orphan deps to be deleted, got count=%d", depCount)
|
||||
}
|
||||
if labelCount != 0 {
|
||||
t.Errorf("expected orphan labels to be deleted, got count=%d", labelCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepairOrphans_CleanDatabase(t *testing.T) {
|
||||
requireTestGuardDisabled(t)
|
||||
|
||||
if testing.Short() {
|
||||
t.Skip("skipping slow repair test in short mode")
|
||||
}
|
||||
|
||||
bdExe := buildBDForTest(t)
|
||||
ws := mkTmpDirInTmp(t, "bd-repair-clean-*")
|
||||
dbPath := filepath.Join(ws, ".beads", "beads.db")
|
||||
|
||||
// Initialize with a clean database
|
||||
if _, err := runBDSideDB(t, bdExe, ws, dbPath, "init", "--prefix", "test", "--quiet"); err != nil {
|
||||
t.Fatalf("bd init failed: %v", err)
|
||||
}
|
||||
if _, err := runBDSideDB(t, bdExe, ws, dbPath, "create", "Issue 1", "-p", "1"); err != nil {
|
||||
t.Fatalf("bd create failed: %v", err)
|
||||
}
|
||||
|
||||
// Run repair on clean database - use --path to bypass normal db init
|
||||
out, err := runBDRepair(t, bdExe, ws)
|
||||
if err != nil {
|
||||
t.Fatalf("bd repair failed: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
// Should report no orphans found
|
||||
if !strings.Contains(out, "No orphaned references found") {
|
||||
t.Errorf("expected clean database message, got: %s", out)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user