From e9e0d7f1e5bd6948b9008c3e729afc6c61666a1e Mon Sep 17 00:00:00 2001 From: Peter Chanthamynavong Date: Tue, 6 Jan 2026 19:13:49 -0800 Subject: [PATCH] feat(daemon): add BD_SOCKET env var for test isolation (#914) Add BD_SOCKET environment variable support to override daemon socket path, enabling parallel test isolation via t.TempDir() + t.Setenv(). Changes: - getSocketPath() checks BD_SOCKET first, falls back to dbPath-derived path - getSocketPathForPID() checks BD_SOCKET first (for consistency) - Add daemon_socket_test.go with isolation pattern examples This is a minimal tracer bullet to validate the approach before expanding to full test isolation infrastructure. Backward compatible: default behavior unchanged without env var set. --- cmd/bd/daemon_autostart.go | 7 +++- cmd/bd/daemon_config.go | 7 +++- cmd/bd/daemon_socket_test.go | 75 ++++++++++++++++++++++++++++++++++++ go.mod | 7 +++- 4 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 cmd/bd/daemon_socket_test.go diff --git a/cmd/bd/daemon_autostart.go b/cmd/bd/daemon_autostart.go index 9092e768..7281e682 100644 --- a/cmd/bd/daemon_autostart.go +++ b/cmd/bd/daemon_autostart.go @@ -455,9 +455,14 @@ func recordDaemonStartFailure() { // No cap needed - backoff is capped at 120s in canRetryDaemonStart } -// getSocketPath returns the daemon socket path based on the database location +// getSocketPath returns the daemon socket path based on the database location. +// If BD_SOCKET env var is set, uses that value instead (enables test isolation). // Returns local socket path (.beads/bd.sock relative to database) func getSocketPath() string { + // Check environment variable first (enables test isolation) + if socketPath := os.Getenv("BD_SOCKET"); socketPath != "" { + return socketPath + } return filepath.Join(filepath.Dir(dbPath), "bd.sock") } diff --git a/cmd/bd/daemon_config.go b/cmd/bd/daemon_config.go index a052a50b..d18bfa8e 100644 --- a/cmd/bd/daemon_config.go +++ b/cmd/bd/daemon_config.go @@ -58,8 +58,13 @@ func getEnvBool(key string, defaultValue bool) bool { return defaultValue } -// getSocketPathForPID determines the socket path for a given PID file +// getSocketPathForPID determines the socket path for a given PID file. +// If BD_SOCKET env var is set, uses that value instead. func getSocketPathForPID(pidFile string) string { + // Check environment variable first (enables test isolation) + if socketPath := os.Getenv("BD_SOCKET"); socketPath != "" { + return socketPath + } // Socket is in same directory as PID file return filepath.Join(filepath.Dir(pidFile), "bd.sock") } diff --git a/cmd/bd/daemon_socket_test.go b/cmd/bd/daemon_socket_test.go new file mode 100644 index 00000000..9c4d5678 --- /dev/null +++ b/cmd/bd/daemon_socket_test.go @@ -0,0 +1,75 @@ +package main + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestSocketPathEnvOverride verifies that BD_SOCKET env var overrides default socket path. +func TestSocketPathEnvOverride(t *testing.T) { + // Create isolated temp directory + tmpDir := t.TempDir() + customSocket := filepath.Join(tmpDir, "custom.sock") + + // Set environment for isolation + t.Setenv("BD_SOCKET", customSocket) + + // Verify getSocketPath returns custom path + got := getSocketPath() + assert.Equal(t, customSocket, got) +} + +// TestSocketPathForPIDEnvOverride verifies that BD_SOCKET env var overrides PID-derived path. +func TestSocketPathForPIDEnvOverride(t *testing.T) { + // Create isolated temp directory + tmpDir := t.TempDir() + customSocket := filepath.Join(tmpDir, "custom.sock") + + // Set environment for isolation + t.Setenv("BD_SOCKET", customSocket) + + // Verify getSocketPathForPID returns custom path (ignoring pidFile) + pidFile := "/some/other/path/daemon.pid" + got := getSocketPathForPID(pidFile) + assert.Equal(t, customSocket, got) +} + +// TestSocketPathDefaultBehavior verifies default behavior when BD_SOCKET is not set. +func TestSocketPathDefaultBehavior(t *testing.T) { + // Ensure BD_SOCKET is not set (t.Setenv restores after test) + t.Setenv("BD_SOCKET", "") + + // Verify getSocketPathForPID derives from PID file path + pidFile := "/path/to/.beads/daemon.pid" + got := getSocketPathForPID(pidFile) + assert.Equal(t, "/path/to/.beads/bd.sock", got) +} + +// TestDaemonSocketIsolation demonstrates that two test instances can use different sockets. +// This is the key pattern for parallel test isolation. +func TestDaemonSocketIsolation(t *testing.T) { + // Simulate two parallel tests with different socket paths + tests := []struct { + name string + sockSuffix string + }{ + {"instance_a", "a.sock"}, + {"instance_b", "b.sock"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Each sub-test gets isolated socket path in its own temp dir + socketPath := filepath.Join(t.TempDir(), tt.sockSuffix) + t.Setenv("BD_SOCKET", socketPath) + + got := getSocketPath() + assert.Equal(t, socketPath, got) + + // Verify paths are unique per instance + assert.Contains(t, got, tt.sockSuffix) + }) + } +} diff --git a/go.mod b/go.mod index 47f883af..c753b633 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,12 @@ require ( github.com/charmbracelet/huh v0.8.0 github.com/charmbracelet/lipgloss v1.1.0 github.com/fsnotify/fsnotify v1.9.0 + github.com/muesli/termenv v0.16.0 github.com/ncruces/go-sqlite3 v0.30.4 + github.com/olebedev/when v1.1.0 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 + github.com/stretchr/testify v1.11.1 github.com/tetratelabs/wazero v1.11.0 golang.org/x/mod v0.31.0 golang.org/x/sys v0.39.0 @@ -34,6 +37,7 @@ require ( github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect @@ -46,11 +50,10 @@ require ( github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.16.0 // indirect github.com/ncruces/julianday v1.0.0 // indirect - github.com/olebedev/when v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.8.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect