When metadata.json has an empty database field and no beads.db file exists,
filepath.Dir("") returns "." which causes lock file operations to use the
current directory instead of the beads directory. This could lead to stack
overflow or unexpected behavior.
Add early validation in tryAutoStartDaemon to check if dbPath is empty and
return false gracefully, consistent with other early-return conditions.
631 lines
17 KiB
Go
631 lines
17 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/beads/internal/config"
|
|
)
|
|
|
|
func tempSockDir(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
base := "/tmp"
|
|
if runtime.GOOS == windowsOS {
|
|
base = os.TempDir()
|
|
} else if _, err := os.Stat(base); err != nil {
|
|
base = os.TempDir()
|
|
}
|
|
|
|
d, err := os.MkdirTemp(base, "bd-sock-*")
|
|
if err != nil {
|
|
t.Fatalf("MkdirTemp: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = os.RemoveAll(d) })
|
|
return d
|
|
}
|
|
|
|
func startTestRPCServer(t *testing.T) (socketPath string, cleanup func()) {
|
|
t.Helper()
|
|
|
|
if isSandboxed() {
|
|
t.Skip("sandboxed environment blocks unix socket operations")
|
|
}
|
|
|
|
tmpDir := tempSockDir(t)
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0o750); err != nil {
|
|
t.Fatalf("MkdirAll: %v", err)
|
|
}
|
|
|
|
socketPath = filepath.Join(beadsDir, "bd.sock")
|
|
db := filepath.Join(beadsDir, "test.db")
|
|
store := newTestStore(t, db)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
log := newTestLogger()
|
|
|
|
server, _, err := startRPCServer(ctx, socketPath, store, tmpDir, db, log)
|
|
if err != nil {
|
|
if errors.Is(err, syscall.EPERM) || errors.Is(err, syscall.EACCES) || os.IsPermission(err) {
|
|
cancel()
|
|
t.Skipf("unix sockets not permitted in this environment: %v", err)
|
|
}
|
|
cancel()
|
|
t.Fatalf("startRPCServer: %v", err)
|
|
}
|
|
|
|
cleanup = func() {
|
|
cancel()
|
|
if server != nil {
|
|
_ = server.Stop()
|
|
}
|
|
}
|
|
|
|
return socketPath, cleanup
|
|
}
|
|
|
|
func captureStderr(t *testing.T, fn func()) string {
|
|
t.Helper()
|
|
|
|
old := os.Stderr
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatalf("os.Pipe: %v", err)
|
|
}
|
|
os.Stderr = w
|
|
|
|
var buf bytes.Buffer
|
|
done := make(chan struct{})
|
|
go func() {
|
|
_, _ = io.Copy(&buf, r)
|
|
close(done)
|
|
}()
|
|
|
|
fn()
|
|
_ = w.Close()
|
|
os.Stderr = old
|
|
<-done
|
|
_ = r.Close()
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
func TestDaemonAutostart_AcquireStartLock_CreatesAndCleansStale(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
lockPath := filepath.Join(tmpDir, "bd.sock.startlock")
|
|
pid, err := readPIDFromFile(lockPath)
|
|
if err == nil || pid != 0 {
|
|
// lock doesn't exist yet; expect read to fail.
|
|
}
|
|
|
|
if !acquireStartLock(lockPath, filepath.Join(tmpDir, "bd.sock")) {
|
|
t.Fatalf("expected acquireStartLock to succeed")
|
|
}
|
|
got, err := readPIDFromFile(lockPath)
|
|
if err != nil {
|
|
t.Fatalf("readPIDFromFile: %v", err)
|
|
}
|
|
if got != os.Getpid() {
|
|
t.Fatalf("expected lock PID %d, got %d", os.Getpid(), got)
|
|
}
|
|
|
|
// Stale lock: dead/unreadable PID should be removed and recreated.
|
|
if err := os.WriteFile(lockPath, []byte("0\n"), 0o600); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
if !acquireStartLock(lockPath, filepath.Join(tmpDir, "bd.sock")) {
|
|
t.Fatalf("expected acquireStartLock to succeed on stale lock")
|
|
}
|
|
got, err = readPIDFromFile(lockPath)
|
|
if err != nil {
|
|
t.Fatalf("readPIDFromFile: %v", err)
|
|
}
|
|
if got != os.Getpid() {
|
|
t.Fatalf("expected recreated lock PID %d, got %d", os.Getpid(), got)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAutostart_AcquireStartLock_CreatesMissingDir(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
socketPath := filepath.Join(tmpDir, "missing", "bd.sock")
|
|
lockPath := socketPath + ".startlock"
|
|
|
|
if _, err := os.Stat(filepath.Dir(lockPath)); !os.IsNotExist(err) {
|
|
t.Fatalf("expected lock dir to be missing before test, got: %v", err)
|
|
}
|
|
|
|
if !acquireStartLock(lockPath, socketPath) {
|
|
t.Fatalf("expected acquireStartLock to succeed when directory missing")
|
|
}
|
|
|
|
if _, err := os.Stat(lockPath); err != nil {
|
|
t.Fatalf("expected lock file to exist, stat error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAutostart_AcquireStartLock_FailsWhenRemoveFails(t *testing.T) {
|
|
// This test verifies that acquireStartLock returns false (instead of
|
|
// recursing infinitely) when os.Remove fails on a stale lock file.
|
|
|
|
oldRemove := removeFileFn
|
|
defer func() { removeFileFn = oldRemove }()
|
|
|
|
// Stub removeFileFn to always fail
|
|
removeFileFn = func(path string) error {
|
|
return os.ErrPermission
|
|
}
|
|
|
|
tmpDir := t.TempDir()
|
|
lockPath := filepath.Join(tmpDir, "bd.sock.startlock")
|
|
socketPath := filepath.Join(tmpDir, "bd.sock")
|
|
|
|
// Create a stale lock file with PID 0 (will be detected as dead)
|
|
if err := os.WriteFile(lockPath, []byte("0\n"), 0o600); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
|
|
// acquireStartLock should return false since it can't remove the stale lock
|
|
// Previously, this would cause infinite recursion and stack overflow
|
|
if acquireStartLock(lockPath, socketPath) {
|
|
t.Fatalf("expected acquireStartLock to fail when remove fails")
|
|
}
|
|
}
|
|
|
|
func TestDaemonAutostart_SocketHealthAndReadiness(t *testing.T) {
|
|
socketPath, cleanup := startTestRPCServer(t)
|
|
defer cleanup()
|
|
|
|
if !canDialSocket(socketPath, 500*time.Millisecond) {
|
|
t.Fatalf("expected canDialSocket to succeed")
|
|
}
|
|
if !isDaemonHealthy(socketPath) {
|
|
t.Fatalf("expected isDaemonHealthy to succeed")
|
|
}
|
|
if !waitForSocketReadiness(socketPath, 500*time.Millisecond) {
|
|
t.Fatalf("expected waitForSocketReadiness to succeed")
|
|
}
|
|
|
|
missing := filepath.Join(tempSockDir(t), "missing.sock")
|
|
if canDialSocket(missing, 50*time.Millisecond) {
|
|
t.Fatalf("expected canDialSocket to fail")
|
|
}
|
|
if waitForSocketReadiness(missing, 200*time.Millisecond) {
|
|
t.Fatalf("expected waitForSocketReadiness to time out")
|
|
}
|
|
}
|
|
|
|
func TestDaemonAutostart_HandleExistingSocket(t *testing.T) {
|
|
socketPath, cleanup := startTestRPCServer(t)
|
|
defer cleanup()
|
|
|
|
if !handleExistingSocket(socketPath) {
|
|
t.Fatalf("expected handleExistingSocket true for running daemon")
|
|
}
|
|
}
|
|
|
|
func TestDaemonAutostart_HandleExistingSocket_StaleCleansUp(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0o750); err != nil {
|
|
t.Fatalf("MkdirAll: %v", err)
|
|
}
|
|
|
|
socketPath := filepath.Join(beadsDir, "bd.sock")
|
|
pidFile := filepath.Join(beadsDir, "daemon.pid")
|
|
if err := os.WriteFile(socketPath, []byte("not-a-socket"), 0o600); err != nil {
|
|
t.Fatalf("WriteFile socket: %v", err)
|
|
}
|
|
if err := os.WriteFile(pidFile, []byte("0\n"), 0o600); err != nil {
|
|
t.Fatalf("WriteFile pid: %v", err)
|
|
}
|
|
|
|
if handleExistingSocket(socketPath) {
|
|
t.Fatalf("expected false for stale socket")
|
|
}
|
|
if _, err := os.Stat(socketPath); !os.IsNotExist(err) {
|
|
t.Fatalf("expected socket removed")
|
|
}
|
|
if _, err := os.Stat(pidFile); !os.IsNotExist(err) {
|
|
t.Fatalf("expected pidfile removed")
|
|
}
|
|
}
|
|
|
|
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(tmpDir, "bd.sock")) {
|
|
t.Fatalf("expected tryAutoStartDaemon to skip due to backoff")
|
|
}
|
|
|
|
daemonStartFailures = 0
|
|
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")
|
|
}
|
|
}
|
|
|
|
func TestDaemonAutostart_MiscHelpers(t *testing.T) {
|
|
if determineSocketPath("/x") != "/x" {
|
|
t.Fatalf("determineSocketPath should be identity")
|
|
}
|
|
|
|
if err := config.Initialize(); err != nil {
|
|
t.Fatalf("config.Initialize: %v", err)
|
|
}
|
|
old := config.GetDuration("flush-debounce")
|
|
defer config.Set("flush-debounce", old)
|
|
|
|
config.Set("flush-debounce", 0)
|
|
if got := getDebounceDuration(); got != 5*time.Second {
|
|
t.Fatalf("expected default debounce 5s, got %v", got)
|
|
}
|
|
config.Set("flush-debounce", 2*time.Second)
|
|
if got := getDebounceDuration(); got != 2*time.Second {
|
|
t.Fatalf("expected debounce 2s, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAutostart_EmitVerboseWarning(t *testing.T) {
|
|
old := daemonStatus
|
|
defer func() { daemonStatus = old }()
|
|
|
|
daemonStatus.SocketPath = "/tmp/bd.sock"
|
|
for _, tt := range []struct {
|
|
reason string
|
|
shouldWrite bool
|
|
}{
|
|
{FallbackConnectFailed, true},
|
|
{FallbackHealthFailed, true},
|
|
{FallbackAutoStartDisabled, true},
|
|
{FallbackAutoStartFailed, true},
|
|
{FallbackDaemonUnsupported, true},
|
|
{FallbackWorktreeSafety, false},
|
|
{FallbackFlagNoDaemon, false},
|
|
} {
|
|
t.Run(tt.reason, func(t *testing.T) {
|
|
daemonStatus.FallbackReason = tt.reason
|
|
out := captureStderr(t, emitVerboseWarning)
|
|
if tt.shouldWrite && out == "" {
|
|
t.Fatalf("expected output")
|
|
}
|
|
if !tt.shouldWrite && out != "" {
|
|
t.Fatalf("expected no output, got %q", out)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDaemonAutostart_StartDaemonProcess_Stubbed(t *testing.T) {
|
|
oldExec := execCommandFn
|
|
oldWait := waitForSocketReadinessFn
|
|
oldCfg := configureDaemonProcessFn
|
|
defer func() {
|
|
execCommandFn = oldExec
|
|
waitForSocketReadinessFn = oldWait
|
|
configureDaemonProcessFn = oldCfg
|
|
}()
|
|
|
|
execCommandFn = func(string, ...string) *exec.Cmd {
|
|
return exec.Command(os.Args[0], "-test.run=^$")
|
|
}
|
|
waitForSocketReadinessFn = func(string, time.Duration) bool { return true }
|
|
configureDaemonProcessFn = func(*exec.Cmd) {}
|
|
|
|
if !startDaemonProcess(filepath.Join(t.TempDir(), "bd.sock")) {
|
|
t.Fatalf("expected startDaemonProcess true when readiness stubbed")
|
|
}
|
|
}
|
|
|
|
func TestDaemonAutostart_StartDaemonProcess_NoGitRepo(t *testing.T) {
|
|
// Test that startDaemonProcess returns false immediately when not in a git repo
|
|
tmpDir := t.TempDir()
|
|
oldDir, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("Getwd: %v", err)
|
|
}
|
|
defer func() {
|
|
_ = os.Chdir(oldDir)
|
|
}()
|
|
|
|
// Change to a temp directory that is NOT a git repo
|
|
if err := os.Chdir(tmpDir); err != nil {
|
|
t.Fatalf("Chdir: %v", err)
|
|
}
|
|
|
|
// Capture stderr to verify the message
|
|
output := captureStderr(t, func() {
|
|
result := startDaemonProcess(filepath.Join(tmpDir, "bd.sock"))
|
|
if result {
|
|
t.Errorf("expected startDaemonProcess to return false when not in git repo")
|
|
}
|
|
})
|
|
|
|
// Verify the correct message is shown
|
|
if !strings.Contains(output, "No git repository initialized") {
|
|
t.Errorf("expected output to contain 'No git repository initialized', got: %q", output)
|
|
}
|
|
if !strings.Contains(output, "running without background sync") {
|
|
t.Errorf("expected output to contain 'running without background sync', got: %q", output)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAutostart_StartDaemonProcess_NoGitRepo_Quiet(t *testing.T) {
|
|
// Test that startDaemonProcess suppresses the note when quietFlag is true
|
|
tmpDir := t.TempDir()
|
|
oldDir, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("Getwd: %v", err)
|
|
}
|
|
oldQuiet := quietFlag
|
|
defer func() {
|
|
_ = os.Chdir(oldDir)
|
|
quietFlag = oldQuiet
|
|
}()
|
|
|
|
// Change to a temp directory that is NOT a git repo
|
|
if err := os.Chdir(tmpDir); err != nil {
|
|
t.Fatalf("Chdir: %v", err)
|
|
}
|
|
|
|
// Enable quiet mode
|
|
quietFlag = true
|
|
|
|
// Capture stderr to verify the message is suppressed
|
|
output := captureStderr(t, func() {
|
|
result := startDaemonProcess(filepath.Join(tmpDir, "bd.sock"))
|
|
if result {
|
|
t.Errorf("expected startDaemonProcess to return false when not in git repo")
|
|
}
|
|
})
|
|
|
|
// Verify the message is NOT shown in quiet mode
|
|
if strings.Contains(output, "No git repository initialized") {
|
|
t.Errorf("expected no output in quiet mode, got: %q", output)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAutostart_RestartDaemonForVersionMismatch_Stubbed(t *testing.T) {
|
|
oldExec := execCommandFn
|
|
oldWait := waitForSocketReadinessFn
|
|
oldRun := isDaemonRunningFn
|
|
oldCfg := configureDaemonProcessFn
|
|
defer func() {
|
|
execCommandFn = oldExec
|
|
waitForSocketReadinessFn = oldWait
|
|
isDaemonRunningFn = oldRun
|
|
configureDaemonProcessFn = oldCfg
|
|
}()
|
|
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0o750); err != nil {
|
|
t.Fatalf("MkdirAll: %v", err)
|
|
}
|
|
oldDB := dbPath
|
|
defer func() { dbPath = oldDB }()
|
|
dbPath = filepath.Join(beadsDir, "test.db")
|
|
|
|
pidFile, err := getPIDFilePath()
|
|
if err != nil {
|
|
t.Fatalf("getPIDFilePath: %v", err)
|
|
}
|
|
sock := getSocketPath()
|
|
// Create socket directory if needed (GH#1001 - socket may be in /tmp/beads-{hash}/)
|
|
sockDir := filepath.Dir(sock)
|
|
if err := os.MkdirAll(sockDir, 0o750); err != nil {
|
|
t.Fatalf("MkdirAll sockDir: %v", err)
|
|
}
|
|
if err := os.WriteFile(pidFile, []byte("999999\n"), 0o600); err != nil {
|
|
t.Fatalf("WriteFile pid: %v", err)
|
|
}
|
|
if err := os.WriteFile(sock, []byte("stale"), 0o600); err != nil {
|
|
t.Fatalf("WriteFile sock: %v", err)
|
|
}
|
|
|
|
execCommandFn = func(string, ...string) *exec.Cmd {
|
|
return exec.Command(os.Args[0], "-test.run=^$")
|
|
}
|
|
waitForSocketReadinessFn = func(string, time.Duration) bool { return true }
|
|
isDaemonRunningFn = func(string) (bool, int) { return false, 0 }
|
|
configureDaemonProcessFn = func(*exec.Cmd) {}
|
|
|
|
if !restartDaemonForVersionMismatch() {
|
|
t.Fatalf("expected restartDaemonForVersionMismatch true when stubbed")
|
|
}
|
|
if _, err := os.Stat(pidFile); !os.IsNotExist(err) {
|
|
t.Fatalf("expected pidfile removed")
|
|
}
|
|
if _, err := os.Stat(sock); !os.IsNotExist(err) {
|
|
t.Fatalf("expected socket removed")
|
|
}
|
|
}
|
|
|
|
// TestIsWispOperation tests the wisp operation detection for auto-daemon-bypass (bd-ta4r)
|
|
func TestIsWispOperation(t *testing.T) {
|
|
// Helper to create a command with parent hierarchy
|
|
makeCmd := func(names ...string) *cobra.Command {
|
|
var current *cobra.Command
|
|
for i, name := range names {
|
|
cmd := &cobra.Command{Use: name}
|
|
if i == 0 {
|
|
current = cmd
|
|
} else {
|
|
current.AddCommand(cmd)
|
|
current = cmd
|
|
}
|
|
}
|
|
return current
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
cmdNames []string // hierarchy: root, child, grandchild...
|
|
args []string
|
|
want bool
|
|
}{
|
|
// Wisp subcommands
|
|
{
|
|
name: "mol wisp (direct)",
|
|
cmdNames: []string{"bd", "mol", "wisp"},
|
|
args: []string{},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "mol wisp create",
|
|
cmdNames: []string{"bd", "mol", "wisp", "create"},
|
|
args: []string{"some-proto"},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "mol wisp list",
|
|
cmdNames: []string{"bd", "mol", "wisp", "list"},
|
|
args: []string{},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "mol wisp gc",
|
|
cmdNames: []string{"bd", "mol", "wisp", "gc"},
|
|
args: []string{},
|
|
want: true,
|
|
},
|
|
// mol burn and squash (wisp-only operations)
|
|
{
|
|
name: "mol burn",
|
|
cmdNames: []string{"bd", "mol", "burn"},
|
|
args: []string{"bd-wisp-abc"},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "mol squash",
|
|
cmdNames: []string{"bd", "mol", "squash"},
|
|
args: []string{"bd-wisp-abc"},
|
|
want: true,
|
|
},
|
|
// Ephemeral issue IDs in args (wisp-* pattern)
|
|
{
|
|
name: "close with bd-wisp ID",
|
|
cmdNames: []string{"bd", "close"},
|
|
args: []string{"bd-wisp-abc123"},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "show with gt-wisp ID",
|
|
cmdNames: []string{"bd", "show"},
|
|
args: []string{"gt-wisp-xyz"},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "update with wisp- prefix",
|
|
cmdNames: []string{"bd", "update"},
|
|
args: []string{"wisp-test", "--status=closed"},
|
|
want: true,
|
|
},
|
|
// Legacy eph-* pattern (backwards compatibility)
|
|
{
|
|
name: "close with legacy bd-eph ID",
|
|
cmdNames: []string{"bd", "close"},
|
|
args: []string{"bd-eph-abc123"},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "show with legacy gt-eph ID",
|
|
cmdNames: []string{"bd", "show"},
|
|
args: []string{"gt-eph-xyz"},
|
|
want: true,
|
|
},
|
|
// Non-wisp operations (should NOT bypass)
|
|
{
|
|
name: "regular show",
|
|
cmdNames: []string{"bd", "show"},
|
|
args: []string{"bd-abc123"},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "regular close",
|
|
cmdNames: []string{"bd", "close"},
|
|
args: []string{"bd-xyz"},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "mol pour (persistent)",
|
|
cmdNames: []string{"bd", "mol", "pour"},
|
|
args: []string{"some-formula"},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "list command",
|
|
cmdNames: []string{"bd", "list"},
|
|
args: []string{},
|
|
want: false,
|
|
},
|
|
// Edge cases
|
|
{
|
|
name: "flag that looks like wisp ID should be ignored",
|
|
cmdNames: []string{"bd", "show"},
|
|
args: []string{"--format=bd-wisp-style", "bd-regular"},
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cmd := makeCmd(tt.cmdNames...)
|
|
got := isWispOperation(cmd, tt.args)
|
|
if got != tt.want {
|
|
t.Errorf("isWispOperation() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
}
|