diff --git a/cmd/bd/daemon_autostart.go b/cmd/bd/daemon_autostart.go index 23b5074d..7df83b48 100644 --- a/cmd/bd/daemon_autostart.go +++ b/cmd/bd/daemon_autostart.go @@ -256,6 +256,14 @@ func tryAutoStartDaemon(socketPath string) bool { return false } + // Empty dbPath causes filepath.Dir("") to return "." which breaks lock + // file operations. This can happen when metadata.json has an empty database + // field and no beads.db file exists. Skip daemon start gracefully. + if dbPath == "" { + debugLog("skipping auto-start: no database path configured") + return false + } + if !canRetryDaemonStart() { debugLog("skipping auto-start due to recent failures") return false diff --git a/cmd/bd/daemon_autostart_unit_test.go b/cmd/bd/daemon_autostart_unit_test.go index 62a332c6..05d2a2a4 100644 --- a/cmd/bd/daemon_autostart_unit_test.go +++ b/cmd/bd/daemon_autostart_unit_test.go @@ -245,14 +245,24 @@ func TestDaemonAutostart_HandleExistingSocket_StaleCleansUp(t *testing.T) { func TestDaemonAutostart_TryAutoStartDaemon_EarlyExits(t *testing.T) { oldFailures := daemonStartFailures oldLast := lastDaemonStartAttempt + oldDbPath := dbPath defer func() { daemonStartFailures = oldFailures lastDaemonStartAttempt = oldLast + dbPath = oldDbPath }() + // Set up a valid dbPath to pass the empty dbPath check (GH#1288) + tmpDir := t.TempDir() + beadsDir := filepath.Join(tmpDir, ".beads") + if err := os.MkdirAll(beadsDir, 0o750); err != nil { + t.Fatalf("MkdirAll: %v", err) + } + dbPath = filepath.Join(beadsDir, "test.db") + daemonStartFailures = 1 lastDaemonStartAttempt = time.Now() - if tryAutoStartDaemon(filepath.Join(t.TempDir(), "bd.sock")) { + if tryAutoStartDaemon(filepath.Join(tmpDir, "bd.sock")) { t.Fatalf("expected tryAutoStartDaemon to skip due to backoff") } @@ -260,6 +270,8 @@ func TestDaemonAutostart_TryAutoStartDaemon_EarlyExits(t *testing.T) { lastDaemonStartAttempt = time.Time{} socketPath, cleanup := startTestRPCServer(t) defer cleanup() + // Update dbPath to match the test server's directory + dbPath = filepath.Join(filepath.Dir(socketPath), "test.db") if !tryAutoStartDaemon(socketPath) { t.Fatalf("expected tryAutoStartDaemon true when daemon already healthy") } @@ -598,3 +610,21 @@ func TestIsWispOperation(t *testing.T) { }) } } + +// TestTryAutoStartDaemon_EmptyDbPath verifies that tryAutoStartDaemon returns +// false early when dbPath is empty, preventing stack overflow from +// filepath.Dir("") returning "." (GH#1288) +func TestTryAutoStartDaemon_EmptyDbPath(t *testing.T) { + // Save and restore global state + oldDbPath := dbPath + defer func() { dbPath = oldDbPath }() + + // Set dbPath to empty string (simulates corrupted metadata.json) + dbPath = "" + + // tryAutoStartDaemon should return false without crashing + result := tryAutoStartDaemon("/tmp/test.sock") + if result { + t.Errorf("tryAutoStartDaemon() = true, want false when dbPath is empty") + } +}