diff --git a/cmd/bd/daemon_health_freebsd.go b/cmd/bd/daemon_health_freebsd.go new file mode 100644 index 00000000..7e0b0451 --- /dev/null +++ b/cmd/bd/daemon_health_freebsd.go @@ -0,0 +1,25 @@ +//go:build freebsd && !wasm + +package main + +import ( + "golang.org/x/sys/unix" +) + +// checkDiskSpace returns the available disk space in MB for the given path. +// Returns (availableMB, true) on success, (0, false) on failure. +func checkDiskSpace(path string) (uint64, bool) { + var stat unix.Statfs_t + if err := unix.Statfs(path, &stat); err != nil { + return 0, false + } + + if stat.Bavail < 0 { + return 0, true + } + + availableBytes := uint64(stat.Bavail) * stat.Bsize //nolint:gosec + availableMB := availableBytes / (1024 * 1024) + + return availableMB, true +} diff --git a/cmd/bd/daemon_health_unix.go b/cmd/bd/daemon_health_unix.go index de1a220d..4ba292d0 100644 --- a/cmd/bd/daemon_health_unix.go +++ b/cmd/bd/daemon_health_unix.go @@ -1,4 +1,4 @@ -//go:build !windows && !wasm +//go:build !windows && !wasm && !freebsd package main @@ -14,8 +14,8 @@ func checkDiskSpace(path string) (uint64, bool) { return 0, false } - // Calculate available space in bytes, then convert to MB - // Bavail is uint64, Bsize is int64; overflow is intentional/safe in this context + // Calculate available space in bytes, then convert to MB. + // On most unix platforms, Bavail is unsigned but Bsize is signed. availableBytes := stat.Bavail * uint64(stat.Bsize) //nolint:gosec availableMB := availableBytes / (1024 * 1024) diff --git a/internal/rpc/limits_test.go b/internal/rpc/limits_test.go index 0191201a..80ac6c61 100644 --- a/internal/rpc/limits_test.go +++ b/internal/rpc/limits_test.go @@ -36,7 +36,7 @@ func TestConnectionLimits(t *testing.T) { } defer store.Close() - socketPath := filepath.Join(tmpDir, "test.sock") + socketPath := newTestSocketPath(t) // Set low connection limit for testing os.Setenv("BEADS_DAEMON_MAX_CONNS", "5") @@ -158,7 +158,7 @@ func TestRequestTimeout(t *testing.T) { } defer store.Close() - socketPath := filepath.Join(tmpDir, "test.sock") + socketPath := newTestSocketPath(t) // Set very short timeout for testing os.Setenv("BEADS_DAEMON_REQUEST_TIMEOUT", "100ms") @@ -199,14 +199,9 @@ func TestRequestTimeout(t *testing.T) { } func TestHealthResponseIncludesLimits(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "bd-limits-test-*") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDir) - + tmpDir := t.TempDir() dbPath := filepath.Join(tmpDir, "test.db") - socketPath := filepath.Join(tmpDir, "test.sock") + socketPath := newTestSocketPath(t) store, err := sqlite.New(context.Background(), dbPath) if err != nil { diff --git a/internal/rpc/status_test.go b/internal/rpc/status_test.go index 5b219529..3dfc1d86 100644 --- a/internal/rpc/status_test.go +++ b/internal/rpc/status_test.go @@ -13,7 +13,7 @@ import ( func TestStatusEndpoint(t *testing.T) { tmpDir := t.TempDir() dbPath := filepath.Join(tmpDir, "test.db") - socketPath := filepath.Join(tmpDir, "test.sock") + socketPath := newTestSocketPath(t) store, err := sqlite.New(context.Background(), dbPath) if err != nil { @@ -87,8 +87,7 @@ func TestStatusEndpoint(t *testing.T) { func TestStatusEndpointWithConfig(t *testing.T) { tmpDir := t.TempDir() dbPath := filepath.Join(tmpDir, "test.db") - socketPath := filepath.Join(tmpDir, "test.sock") - + socketPath := newTestSocketPath(t) store, err := sqlite.New(context.Background(), dbPath) if err != nil { t.Fatalf("failed to create storage: %v", err) @@ -146,8 +145,7 @@ func TestStatusEndpointWithConfig(t *testing.T) { func TestStatusEndpointLocalMode(t *testing.T) { tmpDir := t.TempDir() dbPath := filepath.Join(tmpDir, "test.db") - socketPath := filepath.Join(tmpDir, "test.sock") - + socketPath := newTestSocketPath(t) store, err := sqlite.New(context.Background(), dbPath) if err != nil { t.Fatalf("failed to create storage: %v", err) @@ -205,7 +203,7 @@ func TestStatusEndpointLocalMode(t *testing.T) { func TestStatusEndpointDefaultConfig(t *testing.T) { tmpDir := t.TempDir() dbPath := filepath.Join(tmpDir, "test.db") - socketPath := filepath.Join(tmpDir, "test.sock") + socketPath := newTestSocketPath(t) store, err := sqlite.New(context.Background(), dbPath) if err != nil { @@ -262,7 +260,7 @@ func TestStatusEndpointDefaultConfig(t *testing.T) { func TestSetConfigConcurrency(t *testing.T) { tmpDir := t.TempDir() dbPath := filepath.Join(tmpDir, "test.db") - socketPath := filepath.Join(tmpDir, "test.sock") + socketPath := newTestSocketPath(t) store, err := sqlite.New(context.Background(), dbPath) if err != nil { diff --git a/internal/rpc/test_helpers.go b/internal/rpc/test_helpers.go index d4199306..3979699a 100644 --- a/internal/rpc/test_helpers.go +++ b/internal/rpc/test_helpers.go @@ -2,6 +2,9 @@ package rpc import ( "context" + "os" + "path/filepath" + "runtime" "testing" "github.com/steveyegge/beads/internal/storage/sqlite" @@ -23,6 +26,22 @@ func newTestStore(t *testing.T, dbPath string) *sqlite.SQLiteStorage { _ = store.Close() t.Fatalf("Failed to set issue_prefix: %v", err) } - + return store } + +func newTestSocketPath(t *testing.T) string { + t.Helper() + + // On unix, AF_UNIX socket paths have small length limits (notably on darwin). + // Prefer a short base dir when available. + if runtime.GOOS != "windows" { + d, err := os.MkdirTemp("/tmp", "beads-sock-") + if err == nil { + t.Cleanup(func() { _ = os.RemoveAll(d) }) + return filepath.Join(d, "rpc.sock") + } + } + + return filepath.Join(t.TempDir(), "rpc.sock") +}