doctor: harden corruption repair and JSONL config

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
Jordan Hubbard
2025-12-26 04:29:29 -04:00
parent b089aaa0d6
commit 1a4f06ef8c
17 changed files with 264 additions and 80 deletions

View File

@@ -353,6 +353,28 @@ func applyFixesInteractive(path string, issues []doctorCheck) {
// applyFixList applies a list of fixes and reports results
func applyFixList(path string, fixes []doctorCheck) {
// Run corruption recovery before any operations that need a healthy SQLite DB.
priority := map[string]int{
"Database Integrity": 0,
}
slices.SortStableFunc(fixes, func(a, b doctorCheck) int {
pa, oka := priority[a.Name]
if !oka {
pa = 1000
}
pb, okb := priority[b.Name]
if !okb {
pb = 1000
}
if pa < pb {
return -1
}
if pa > pb {
return 1
}
return 0
})
fixedCount := 0
errorCount := 0

View File

@@ -316,6 +316,10 @@ func checkMetadataConfigValues(repoPath string) []string {
// Validate jsonl_export filename
if cfg.JSONLExport != "" {
switch cfg.JSONLExport {
case "deletions.jsonl", "interactions.jsonl", "molecules.jsonl":
issues = append(issues, fmt.Sprintf("metadata.json jsonl_export: %q is a system file and should not be configured as a JSONL export (expected issues.jsonl)", cfg.JSONLExport))
}
if strings.Contains(cfg.JSONLExport, string(os.PathSeparator)) || strings.Contains(cfg.JSONLExport, "/") {
issues = append(issues, fmt.Sprintf("metadata.json jsonl_export: %q should be a filename, not a path", cfg.JSONLExport))
}

View File

@@ -213,6 +213,21 @@ func TestCheckMetadataConfigValues(t *testing.T) {
t.Error("expected issues for wrong jsonl extension")
}
})
t.Run("jsonl_export cannot be system file", func(t *testing.T) {
metadataContent := `{
"database": "beads.db",
"jsonl_export": "interactions.jsonl"
}`
if err := os.WriteFile(filepath.Join(beadsDir, "metadata.json"), []byte(metadataContent), 0644); err != nil {
t.Fatalf("failed to write metadata.json: %v", err)
}
issues := checkMetadataConfigValues(tmpDir)
if len(issues) == 0 {
t.Error("expected issues for system jsonl_export")
}
})
}
func contains(s, substr string) bool {

View File

@@ -12,6 +12,13 @@ import (
// This prevents fork bombs when tests call functions that execute bd subcommands.
var ErrTestBinary = fmt.Errorf("running as test binary - cannot execute bd subcommands")
func newBdCmd(bdBinary string, args ...string) *exec.Cmd {
fullArgs := append([]string{"--no-daemon"}, args...)
cmd := exec.Command(bdBinary, fullArgs...) // #nosec G204 -- bdBinary from validated executable path
cmd.Env = append(os.Environ(), "BEADS_NO_DAEMON=1")
return cmd
}
// getBdBinary returns the path to the bd binary to use for fix operations.
// It prefers the current executable to avoid command injection attacks.
// Returns ErrTestBinary if running as a test binary to prevent fork bombs.

View File

@@ -3,7 +3,6 @@ package fix
import (
"fmt"
"os"
"os/exec"
"path/filepath"
)
@@ -36,7 +35,7 @@ func Daemon(path string) error {
}
// Run bd daemons killall to clean up stale daemons
cmd := exec.Command(bdBinary, "daemons", "killall") // #nosec G204 -- bdBinary from validated executable path
cmd := newBdCmd(bdBinary, "daemons", "killall")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

View File

@@ -32,6 +32,13 @@ func DatabaseConfig(path string) error {
fixed := false
// Never treat system JSONL files as a JSONL export configuration.
if isSystemJSONLFilename(cfg.JSONLExport) {
fmt.Printf(" Updating jsonl_export: %s → issues.jsonl\n", cfg.JSONLExport)
cfg.JSONLExport = "issues.jsonl"
fixed = true
}
// Check if configured JSONL exists
if cfg.JSONLExport != "" {
jsonlPath := cfg.JSONLPath(beadsDir)
@@ -99,7 +106,15 @@ func findActualJSONLFile(beadsDir string) string {
strings.Contains(lowerName, ".orig") ||
strings.Contains(lowerName, ".bak") ||
strings.Contains(lowerName, "~") ||
strings.HasPrefix(lowerName, "backup_") {
strings.HasPrefix(lowerName, "backup_") ||
// System files are not JSONL exports.
name == "deletions.jsonl" ||
name == "interactions.jsonl" ||
name == "molecules.jsonl" ||
// Git merge conflict artifacts (e.g., issues.base.jsonl, issues.left.jsonl)
strings.Contains(lowerName, ".base.jsonl") ||
strings.Contains(lowerName, ".left.jsonl") ||
strings.Contains(lowerName, ".right.jsonl") {
continue
}
@@ -121,6 +136,15 @@ func findActualJSONLFile(beadsDir string) string {
return candidates[0]
}
func isSystemJSONLFilename(name string) bool {
switch name {
case "deletions.jsonl", "interactions.jsonl", "molecules.jsonl":
return true
default:
return false
}
}
// LegacyJSONLConfig migrates from legacy beads.jsonl to canonical issues.jsonl.
// This renames the file, updates metadata.json, and updates .gitattributes if present.
// bd-6xd: issues.jsonl is the canonical filename

View File

@@ -220,3 +220,53 @@ func TestLegacyJSONLConfig_UpdatesGitattributes(t *testing.T) {
t.Errorf("Expected .gitattributes to reference issues.jsonl, got: %q", string(content))
}
}
// TestFindActualJSONLFile_SkipsSystemFiles ensures system JSONL files are never treated as JSONL exports.
func TestFindActualJSONLFile_SkipsSystemFiles(t *testing.T) {
tmpDir := t.TempDir()
// Only system files → no candidates.
if err := os.WriteFile(filepath.Join(tmpDir, "interactions.jsonl"), []byte(`{"id":"x"}`), 0644); err != nil {
t.Fatal(err)
}
if got := findActualJSONLFile(tmpDir); got != "" {
t.Fatalf("expected empty result, got %q", got)
}
// System + legacy export → legacy wins.
if err := os.WriteFile(filepath.Join(tmpDir, "beads.jsonl"), []byte(`{"id":"x"}`), 0644); err != nil {
t.Fatal(err)
}
if got := findActualJSONLFile(tmpDir); got != "beads.jsonl" {
t.Fatalf("expected beads.jsonl, got %q", got)
}
}
func TestDatabaseConfigFix_RejectsSystemJSONLExport(t *testing.T) {
tmpDir := t.TempDir()
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.Mkdir(beadsDir, 0755); err != nil {
t.Fatalf("Failed to create .beads dir: %v", err)
}
if err := os.WriteFile(filepath.Join(beadsDir, "interactions.jsonl"), []byte(`{"id":"x"}`), 0644); err != nil {
t.Fatalf("Failed to create interactions.jsonl: %v", err)
}
cfg := &configfile.Config{Database: "beads.db", JSONLExport: "interactions.jsonl"}
if err := cfg.Save(beadsDir); err != nil {
t.Fatalf("Failed to save config: %v", err)
}
if err := DatabaseConfig(tmpDir); err != nil {
t.Fatalf("DatabaseConfig failed: %v", err)
}
updated, err := configfile.Load(beadsDir)
if err != nil {
t.Fatalf("Failed to load updated config: %v", err)
}
if updated.JSONLExport != "issues.jsonl" {
t.Fatalf("expected issues.jsonl, got %q", updated.JSONLExport)
}
}

View File

@@ -1,22 +1,18 @@
package fix
import (
"bufio"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/configfile"
"github.com/steveyegge/beads/internal/utils"
)
// DatabaseIntegrity attempts to recover from database corruption by:
// 1. Backing up the corrupt database (and WAL/SHM if present)
// 2. Re-initializing the database from the git-tracked JSONL export
// 2. Re-initializing the database from the working tree JSONL export
//
// This is intentionally conservative: it will not delete JSONL, and it preserves the
// original DB as a backup for forensic recovery.
@@ -74,61 +70,35 @@ func DatabaseIntegrity(path string) error {
}
}
// Rebuild via bd init, pointing at the same db path.
// Rebuild by importing from the working tree JSONL into a fresh database.
bdBinary, err := getBdBinary()
if err != nil {
return err
}
args := []string{"--db", dbPath, "init", "--quiet", "--force", "--skip-hooks", "--skip-merge-driver"}
if prefix := detectPrefixFromJSONL(jsonlPath); prefix != "" {
args = append(args, "--prefix", prefix)
}
cmd := exec.Command(bdBinary, args...) // #nosec G204 -- bdBinary is a validated executable path
// Use import (not init) so we always hydrate from the working tree JSONL, not git-tracked blobs.
args := []string{"--db", dbPath, "import", "-i", jsonlPath, "--force", "--no-git-history"}
cmd := newBdCmd(bdBinary, args...)
cmd.Dir = absPath
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
if err := cmd.Run(); err != nil {
// Best-effort rollback: if init didn't recreate the db, restore the backup.
if _, statErr := os.Stat(dbPath); os.IsNotExist(statErr) {
_ = os.Rename(backupDB, dbPath)
// Best-effort rollback: attempt to restore the backup, preserving any partial init output.
failedTS := time.Now().UTC().Format("20060102T150405Z")
if _, statErr := os.Stat(dbPath); statErr == nil {
failedDB := dbPath + "." + failedTS + ".failed.init.db"
_ = os.Rename(dbPath, failedDB)
for _, suffix := range []string{"-wal", "-shm", "-journal"} {
_ = os.Rename(backupDB+suffix, dbPath+suffix)
_ = os.Rename(dbPath+suffix, failedDB+suffix)
}
}
_ = os.Rename(backupDB, dbPath)
for _, suffix := range []string{"-wal", "-shm", "-journal"} {
_ = os.Rename(backupDB+suffix, dbPath+suffix)
}
return fmt.Errorf("failed to rebuild database from JSONL: %w (backup: %s)", err, backupDB)
}
return nil
}
func detectPrefixFromJSONL(jsonlPath string) string {
f, err := os.Open(jsonlPath) // #nosec G304 -- jsonlPath is within the workspace
if err != nil {
return ""
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Bytes()
if len(line) == 0 {
continue
}
var issue struct {
ID string `json:"id"`
}
if err := json.Unmarshal(line, &issue); err != nil {
continue
}
if issue.ID == "" {
continue
}
return utils.ExtractIssuePrefix(issue.ID)
}
return ""
}

View File

@@ -28,7 +28,7 @@ func GitHooks(path string) error {
}
// Run bd hooks install
cmd := exec.Command(bdBinary, "hooks", "install") // #nosec G204 -- bdBinary from validated executable path
cmd := newBdCmd(bdBinary, "hooks", "install")
cmd.Dir = path // Set working directory without changing process dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

View File

@@ -3,7 +3,6 @@ package fix
import (
"fmt"
"os"
"os/exec"
"path/filepath"
)
@@ -28,7 +27,7 @@ func DatabaseVersion(path string) error {
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
// No database - this is a fresh clone, run bd init
fmt.Println("→ No database found, running 'bd init' to hydrate from JSONL...")
cmd := exec.Command(bdBinary, "init") // #nosec G204 -- bdBinary from validated executable path
cmd := newBdCmd(bdBinary, "init")
cmd.Dir = path
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@@ -41,8 +40,8 @@ func DatabaseVersion(path string) error {
}
// Database exists - run bd migrate
cmd := exec.Command(bdBinary, "migrate") // #nosec G204 -- bdBinary from validated executable path
cmd.Dir = path // Set working directory without changing process dir
cmd := newBdCmd(bdBinary, "migrate")
cmd.Dir = path // Set working directory without changing process dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

View File

@@ -3,7 +3,6 @@ package fix
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
@@ -31,9 +30,9 @@ func readLineUnbuffered() (string, error) {
// RepoFingerprint fixes repo fingerprint mismatches by prompting the user
// for which action to take. This is interactive because the consequences
// differ significantly between options:
// 1. Update repo ID (if URL changed or bd upgraded)
// 2. Reinitialize database (if wrong database was copied)
// 3. Skip (do nothing)
// 1. Update repo ID (if URL changed or bd upgraded)
// 2. Reinitialize database (if wrong database was copied)
// 3. Skip (do nothing)
func RepoFingerprint(path string) error {
// Validate workspace
if err := validateBeadsWorkspace(path); err != nil {
@@ -67,7 +66,7 @@ func RepoFingerprint(path string) error {
case "1":
// Run bd migrate --update-repo-id
fmt.Println(" → Running 'bd migrate --update-repo-id'...")
cmd := exec.Command(bdBinary, "migrate", "--update-repo-id") // #nosec G204 -- bdBinary from validated executable path
cmd := newBdCmd(bdBinary, "migrate", "--update-repo-id")
cmd.Dir = path
cmd.Stdin = os.Stdin // Allow user to respond to migrate's confirmation prompt
cmd.Stdout = os.Stdout
@@ -105,7 +104,7 @@ func RepoFingerprint(path string) error {
_ = os.Remove(dbPath + "-shm")
fmt.Println(" → Running 'bd init'...")
cmd := exec.Command(bdBinary, "init", "--quiet") // #nosec G204 -- bdBinary from validated executable path
cmd := newBdCmd(bdBinary, "init", "--quiet")
cmd.Dir = path
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

View File

@@ -6,7 +6,6 @@ import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
_ "github.com/ncruces/go-sqlite3/driver"
@@ -102,21 +101,36 @@ func DBJSONLSync(path string) error {
return err
}
// Run the appropriate sync command
var cmd *exec.Cmd
if syncDirection == "export" {
// Export DB to JSONL file (must specify -o to write to file, not stdout)
jsonlOutputPath := filepath.Join(beadsDir, "issues.jsonl")
cmd = exec.Command(bdBinary, "export", "-o", jsonlOutputPath, "--force") // #nosec G204 -- bdBinary from validated executable path
} else {
cmd = exec.Command(bdBinary, "sync", "--import-only") // #nosec G204 -- bdBinary from validated executable path
exportCmd := newBdCmd(bdBinary, "export", "-o", jsonlOutputPath, "--force")
exportCmd.Dir = path // Set working directory without changing process dir
exportCmd.Stdout = os.Stdout
exportCmd.Stderr = os.Stderr
if err := exportCmd.Run(); err != nil {
return fmt.Errorf("failed to export database to JSONL: %w", err)
}
// Staleness check uses last_import_time. After exporting, JSONL mtime is newer,
// so mark the DB as fresh by running a no-op import (skip existing issues).
markFreshCmd := newBdCmd(bdBinary, "import", "-i", jsonlOutputPath, "--force", "--skip-existing", "--no-git-history")
markFreshCmd.Dir = path
markFreshCmd.Stdout = os.Stdout
markFreshCmd.Stderr = os.Stderr
if err := markFreshCmd.Run(); err != nil {
return fmt.Errorf("failed to mark database as fresh after export: %w", err)
}
return nil
}
cmd.Dir = path // Set working directory without changing process dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
importCmd := newBdCmd(bdBinary, "sync", "--import-only")
importCmd.Dir = path // Set working directory without changing process dir
importCmd.Stdout = os.Stdout
importCmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
if err := importCmd.Run(); err != nil {
return fmt.Errorf("failed to sync database with JSONL: %w", err)
}

View File

@@ -32,8 +32,7 @@ func SyncBranchConfig(path string) error {
}
// Set sync.branch using bd config set
// #nosec G204 - bdBinary is controlled by getBdBinary() which returns os.Executable()
setCmd := exec.Command(bdBinary, "config", "set", "sync.branch", currentBranch)
setCmd := newBdCmd(bdBinary, "config", "set", "sync.branch", currentBranch)
setCmd.Dir = path
if output, err := setCmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to set sync.branch: %w\nOutput: %s", err, string(output))

View File

@@ -53,7 +53,7 @@ func CheckLegacyBeadsSlashCommands(repoPath string) DoctorCheck {
Name: "Legacy Commands",
Status: "warning",
Message: fmt.Sprintf("Old beads integration detected in %s", strings.Join(filesWithLegacyCommands, ", ")),
Detail: "Found: /beads:* slash command references (deprecated)\n" +
Detail: "Found: /beads:* slash command references (deprecated)\n" +
" These commands are token-inefficient (~10.5k tokens per session)",
Fix: "Migrate to bd prime hooks for better token efficiency:\n" +
"\n" +
@@ -104,7 +104,7 @@ func CheckAgentDocumentation(repoPath string) DoctorCheck {
Name: "Agent Documentation",
Status: "warning",
Message: "No agent documentation found",
Detail: "Missing: AGENTS.md or CLAUDE.md\n" +
Detail: "Missing: AGENTS.md or CLAUDE.md\n" +
" Documenting workflow helps AI agents work more effectively",
Fix: "Add agent documentation:\n" +
" • Run 'bd onboard' to create AGENTS.md with workflow guidance\n" +
@@ -187,7 +187,7 @@ func CheckLegacyJSONLFilename(repoPath string) DoctorCheck {
Name: "JSONL Files",
Status: "warning",
Message: fmt.Sprintf("Multiple JSONL files found: %s", strings.Join(realJSONLFiles, ", ")),
Detail: "Having multiple JSONL files can cause sync and merge conflicts.\n" +
Detail: "Having multiple JSONL files can cause sync and merge conflicts.\n" +
" Only one JSONL file should be used per repository.",
Fix: "Determine which file is current and remove the others:\n" +
" 1. Check 'bd stats' to see which file is being used\n" +
@@ -235,7 +235,7 @@ func CheckLegacyJSONLConfig(repoPath string) DoctorCheck {
Name: "JSONL Config",
Status: "warning",
Message: "Using legacy beads.jsonl filename",
Detail: "The canonical filename is now issues.jsonl (bd-6xd).\n" +
Detail: "The canonical filename is now issues.jsonl (bd-6xd).\n" +
" Legacy beads.jsonl is still supported but should be migrated.",
Fix: "Run 'bd doctor --fix' to auto-migrate, or manually:\n" +
" 1. git mv .beads/beads.jsonl .beads/issues.jsonl\n" +
@@ -251,7 +251,7 @@ func CheckLegacyJSONLConfig(repoPath string) DoctorCheck {
Status: "warning",
Message: "Config references beads.jsonl but issues.jsonl exists",
Detail: "metadata.json says beads.jsonl but the actual file is issues.jsonl",
Fix: "Run 'bd doctor --fix' to update the configuration",
Fix: "Run 'bd doctor --fix' to update the configuration",
}
}
}
@@ -303,6 +303,16 @@ func CheckDatabaseConfig(repoPath string) DoctorCheck {
// Check if configured JSONL exists
if cfg.JSONLExport != "" {
if cfg.JSONLExport == "deletions.jsonl" || cfg.JSONLExport == "interactions.jsonl" || cfg.JSONLExport == "molecules.jsonl" {
return DoctorCheck{
Name: "Database Config",
Status: "error",
Message: fmt.Sprintf("Invalid jsonl_export %q (system file)", cfg.JSONLExport),
Detail: "metadata.json jsonl_export must reference the git-tracked issues export (typically issues.jsonl), not a system log file.",
Fix: "Run 'bd doctor --fix' to reset metadata.json jsonl_export to issues.jsonl, then commit the change.",
}
}
jsonlPath := cfg.JSONLPath(beadsDir)
if _, err := os.Stat(jsonlPath); os.IsNotExist(err) {
// Check if other .jsonl files exist
@@ -315,7 +325,15 @@ func CheckDatabaseConfig(repoPath string) DoctorCheck {
lowerName := strings.ToLower(name)
if !strings.Contains(lowerName, "backup") &&
!strings.Contains(lowerName, ".orig") &&
!strings.Contains(lowerName, ".bak") {
!strings.Contains(lowerName, ".bak") &&
!strings.Contains(lowerName, "~") &&
!strings.HasPrefix(lowerName, "backup_") &&
name != "deletions.jsonl" &&
name != "interactions.jsonl" &&
name != "molecules.jsonl" &&
!strings.Contains(lowerName, ".base.jsonl") &&
!strings.Contains(lowerName, ".left.jsonl") &&
!strings.Contains(lowerName, ".right.jsonl") {
otherJSONLs = append(otherJSONLs, name)
}
}
@@ -421,7 +439,7 @@ func CheckFreshClone(repoPath string) DoctorCheck {
Name: "Fresh Clone",
Status: "warning",
Message: fmt.Sprintf("Fresh clone detected (%d issues in %s, no database)", issueCount, jsonlName),
Detail: "This appears to be a freshly cloned repository.\n" +
Detail: "This appears to be a freshly cloned repository.\n" +
" The JSONL file contains issues but no local database exists.\n" +
" Run 'bd init' to create the database and import existing issues.",
Fix: fmt.Sprintf("Run '%s' to initialize the database and import issues", fixCmd),

View File

@@ -410,6 +410,49 @@ func TestCheckLegacyJSONLConfig(t *testing.T) {
}
}
func TestCheckDatabaseConfig_IgnoresSystemJSONLs(t *testing.T) {
tmpDir := t.TempDir()
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.Mkdir(beadsDir, 0750); err != nil {
t.Fatal(err)
}
// Configure issues.jsonl, but only create interactions.jsonl.
metadataPath := filepath.Join(beadsDir, "metadata.json")
if err := os.WriteFile(metadataPath, []byte(`{"database":"beads.db","jsonl_export":"issues.jsonl"}`), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(beadsDir, "interactions.jsonl"), []byte(`{"id":"x"}`), 0644); err != nil {
t.Fatal(err)
}
check := CheckDatabaseConfig(tmpDir)
if check.Status != "ok" {
t.Fatalf("expected ok, got %s: %s\n%s", check.Status, check.Message, check.Detail)
}
}
func TestCheckDatabaseConfig_SystemJSONLExportIsError(t *testing.T) {
tmpDir := t.TempDir()
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.Mkdir(beadsDir, 0750); err != nil {
t.Fatal(err)
}
metadataPath := filepath.Join(beadsDir, "metadata.json")
if err := os.WriteFile(metadataPath, []byte(`{"database":"beads.db","jsonl_export":"interactions.jsonl"}`), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(beadsDir, "interactions.jsonl"), []byte(`{"id":"x"}`), 0644); err != nil {
t.Fatal(err)
}
check := CheckDatabaseConfig(tmpDir)
if check.Status != "error" {
t.Fatalf("expected error, got %s: %s", check.Status, check.Message)
}
}
func TestCheckFreshClone(t *testing.T) {
tests := []struct {
name string

View File

@@ -67,6 +67,14 @@ func TestDoctorRepair_CorruptDatabase_NoJSONL_FixFails(t *testing.T) {
if !strings.Contains(out, "cannot auto-recover") {
t.Fatalf("expected auto-recover error, got:\n%s", out)
}
// Ensure we don't mis-configure jsonl_export to a system file during failure.
metadata, readErr := os.ReadFile(filepath.Join(ws, ".beads", "metadata.json"))
if readErr == nil {
if strings.Contains(string(metadata), "interactions.jsonl") {
t.Fatalf("unexpected metadata.json jsonl_export set to interactions.jsonl:\n%s", string(metadata))
}
}
}
func TestDoctorRepair_CorruptDatabase_BacksUpSidecars(t *testing.T) {

View File

@@ -623,6 +623,13 @@ var rootCmd = &cobra.Command{
FallbackReason: FallbackNone,
}
// Doctor should always run in direct mode. It's specifically used to diagnose and
// repair daemon/DB issues, so attempting to connect to (or auto-start) a daemon
// can add noise and timeouts.
if cmd.Name() == "doctor" {
noDaemon = true
}
// Try to connect to daemon first (unless --no-daemon flag is set or worktree safety check fails)
if noDaemon {
daemonStatus.FallbackReason = FallbackFlagNoDaemon
@@ -917,8 +924,14 @@ var rootCmd = &cobra.Command{
if store != nil {
_ = store.Close()
}
if profileFile != nil { pprof.StopCPUProfile(); _ = profileFile.Close() }
if traceFile != nil { trace.Stop(); _ = traceFile.Close() }
if profileFile != nil {
pprof.StopCPUProfile()
_ = profileFile.Close()
}
if traceFile != nil {
trace.Stop()
_ = traceFile.Close()
}
// Cancel the signal context to clean up resources
if rootCancel != nil {