fix(daemon): socket path shortening for long workspace paths Fixes GH#1001 where long workspace paths (e.g., pytest temp directories) caused socket path mismatches. The daemon now uses rpc.ShortSocketPath() consistently with the client. Changes: - daemon.go: Use rpc.ShortSocketPath() + EnsureSocketDir() for daemon socket - daemon_config.go: Update getSocketPathForPID() to use rpc.ShortSocketPath() - Added tests for long path handling and client-daemon socket path agreement Co-Authored-By: Eugene Sukhodolin <sukhodolin@users.noreply.github.com>
159 lines
5.2 KiB
Go
159 lines
5.2 KiB
Go
package main
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/steveyegge/beads/internal/rpc"
|
|
)
|
|
|
|
// 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()
|
|
if got != customSocket {
|
|
t.Errorf("getSocketPath() = %q, want %q", got, customSocket)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
if got != customSocket {
|
|
t.Errorf("getSocketPathForPID(%q) = %q, want %q", pidFile, got, customSocket)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
want := "/path/to/.beads/bd.sock"
|
|
if got != want {
|
|
t.Errorf("getSocketPathForPID(%q) = %q, want %q", pidFile, got, want)
|
|
}
|
|
}
|
|
|
|
// TestSocketPathForPIDLongPath verifies that long workspace paths use shortened socket paths.
|
|
// This fixes GH#1001 where pytest temp directories exceeded macOS's 104-byte socket path limit.
|
|
func TestSocketPathForPIDLongPath(t *testing.T) {
|
|
t.Setenv("BD_SOCKET", "")
|
|
|
|
// Create a path that would exceed the 103-byte limit when .beads/bd.sock is appended
|
|
// /long/path/.beads/daemon.pid -> workspace is /long/path
|
|
// socket would be /long/path/.beads/bd.sock
|
|
longWorkspace := "/" + strings.Repeat("a", 90) // 91 bytes
|
|
pidFile := filepath.Join(longWorkspace, ".beads", "daemon.pid")
|
|
|
|
got := getSocketPathForPID(pidFile)
|
|
|
|
// Should NOT be the natural path (which would be too long)
|
|
naturalPath := filepath.Join(longWorkspace, ".beads", "bd.sock")
|
|
if got == naturalPath {
|
|
t.Errorf("getSocketPathForPID should use short path for long workspaces, got natural path %q (%d bytes)",
|
|
got, len(got))
|
|
}
|
|
|
|
// Should be in /tmp/beads-{hash}/
|
|
if !strings.HasPrefix(got, "/tmp/beads-") {
|
|
t.Errorf("getSocketPathForPID(%q) = %q, want path starting with /tmp/beads-", pidFile, got)
|
|
}
|
|
|
|
// Should end with bd.sock
|
|
if !strings.HasSuffix(got, "/bd.sock") {
|
|
t.Errorf("getSocketPathForPID(%q) = %q, want path ending with /bd.sock", pidFile, got)
|
|
}
|
|
|
|
// Should be under the limit
|
|
if len(got) > 103 {
|
|
t.Errorf("getSocketPathForPID returned path of %d bytes, want <= 103", len(got))
|
|
}
|
|
}
|
|
|
|
// TestSocketPathForPIDClientDaemonAgreement verifies that getSocketPathForPID
|
|
// returns the same path as rpc.ShortSocketPath for the same workspace.
|
|
// This is critical - if they disagree, the daemon listens on one path while
|
|
// the client tries to connect to another, causing connection failures.
|
|
// This test caught the GH#1001 bug where daemon.go used filepath.Join directly
|
|
// instead of rpc.ShortSocketPath.
|
|
func TestSocketPathForPIDClientDaemonAgreement(t *testing.T) {
|
|
t.Setenv("BD_SOCKET", "")
|
|
|
|
tests := []struct {
|
|
name string
|
|
workspacePath string
|
|
}{
|
|
{"short_path", "/home/user/project"},
|
|
{"medium_path", "/Users/testuser/Documents/projects/myapp"},
|
|
{"long_path", "/" + strings.Repeat("a", 90)}, // Forces short socket path
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// What getSocketPathForPID returns (used by daemon operations)
|
|
pidFile := filepath.Join(tt.workspacePath, ".beads", "daemon.pid")
|
|
fromPID := getSocketPathForPID(pidFile)
|
|
|
|
// What rpc.ShortSocketPath returns (used by client via getSocketPath)
|
|
fromRPC := rpc.ShortSocketPath(tt.workspacePath)
|
|
|
|
if fromPID != fromRPC {
|
|
t.Errorf("socket path mismatch for workspace %q:\n getSocketPathForPID: %q\n rpc.ShortSocketPath: %q",
|
|
tt.workspacePath, fromPID, fromRPC)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
if got != socketPath {
|
|
t.Errorf("getSocketPath() = %q, want %q", got, socketPath)
|
|
}
|
|
|
|
// Verify paths are unique per instance
|
|
if !strings.Contains(got, tt.sockSuffix) {
|
|
t.Errorf("getSocketPath() = %q, want it to contain %q", got, tt.sockSuffix)
|
|
}
|
|
})
|
|
}
|
|
}
|