fix(unix): handle Statfs field types for disk space check (#646)

* fix(unix): handle Statfs field types for disk space check

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* fix(freebsd): build disk space check without type mismatch

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
Jordan Hubbard
2025-12-19 17:50:14 -08:00
committed by GitHub
parent 13a471fe45
commit b69df499ea
5 changed files with 57 additions and 20 deletions

View File

@@ -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
}

View File

@@ -1,4 +1,4 @@
//go:build !windows && !wasm //go:build !windows && !wasm && !freebsd
package main package main
@@ -14,8 +14,8 @@ func checkDiskSpace(path string) (uint64, bool) {
return 0, false return 0, false
} }
// Calculate available space in bytes, then convert to MB // Calculate available space in bytes, then convert to MB.
// Bavail is uint64, Bsize is int64; overflow is intentional/safe in this context // On most unix platforms, Bavail is unsigned but Bsize is signed.
availableBytes := stat.Bavail * uint64(stat.Bsize) //nolint:gosec availableBytes := stat.Bavail * uint64(stat.Bsize) //nolint:gosec
availableMB := availableBytes / (1024 * 1024) availableMB := availableBytes / (1024 * 1024)

View File

@@ -36,7 +36,7 @@ func TestConnectionLimits(t *testing.T) {
} }
defer store.Close() defer store.Close()
socketPath := filepath.Join(tmpDir, "test.sock") socketPath := newTestSocketPath(t)
// Set low connection limit for testing // Set low connection limit for testing
os.Setenv("BEADS_DAEMON_MAX_CONNS", "5") os.Setenv("BEADS_DAEMON_MAX_CONNS", "5")
@@ -158,7 +158,7 @@ func TestRequestTimeout(t *testing.T) {
} }
defer store.Close() defer store.Close()
socketPath := filepath.Join(tmpDir, "test.sock") socketPath := newTestSocketPath(t)
// Set very short timeout for testing // Set very short timeout for testing
os.Setenv("BEADS_DAEMON_REQUEST_TIMEOUT", "100ms") os.Setenv("BEADS_DAEMON_REQUEST_TIMEOUT", "100ms")
@@ -199,14 +199,9 @@ func TestRequestTimeout(t *testing.T) {
} }
func TestHealthResponseIncludesLimits(t *testing.T) { func TestHealthResponseIncludesLimits(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "bd-limits-test-*") tmpDir := t.TempDir()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
dbPath := filepath.Join(tmpDir, "test.db") dbPath := filepath.Join(tmpDir, "test.db")
socketPath := filepath.Join(tmpDir, "test.sock") socketPath := newTestSocketPath(t)
store, err := sqlite.New(context.Background(), dbPath) store, err := sqlite.New(context.Background(), dbPath)
if err != nil { if err != nil {

View File

@@ -13,7 +13,7 @@ import (
func TestStatusEndpoint(t *testing.T) { func TestStatusEndpoint(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db") dbPath := filepath.Join(tmpDir, "test.db")
socketPath := filepath.Join(tmpDir, "test.sock") socketPath := newTestSocketPath(t)
store, err := sqlite.New(context.Background(), dbPath) store, err := sqlite.New(context.Background(), dbPath)
if err != nil { if err != nil {
@@ -87,8 +87,7 @@ func TestStatusEndpoint(t *testing.T) {
func TestStatusEndpointWithConfig(t *testing.T) { func TestStatusEndpointWithConfig(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db") dbPath := filepath.Join(tmpDir, "test.db")
socketPath := filepath.Join(tmpDir, "test.sock") socketPath := newTestSocketPath(t)
store, err := sqlite.New(context.Background(), dbPath) store, err := sqlite.New(context.Background(), dbPath)
if err != nil { if err != nil {
t.Fatalf("failed to create storage: %v", err) t.Fatalf("failed to create storage: %v", err)
@@ -146,8 +145,7 @@ func TestStatusEndpointWithConfig(t *testing.T) {
func TestStatusEndpointLocalMode(t *testing.T) { func TestStatusEndpointLocalMode(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db") dbPath := filepath.Join(tmpDir, "test.db")
socketPath := filepath.Join(tmpDir, "test.sock") socketPath := newTestSocketPath(t)
store, err := sqlite.New(context.Background(), dbPath) store, err := sqlite.New(context.Background(), dbPath)
if err != nil { if err != nil {
t.Fatalf("failed to create storage: %v", err) t.Fatalf("failed to create storage: %v", err)
@@ -205,7 +203,7 @@ func TestStatusEndpointLocalMode(t *testing.T) {
func TestStatusEndpointDefaultConfig(t *testing.T) { func TestStatusEndpointDefaultConfig(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db") dbPath := filepath.Join(tmpDir, "test.db")
socketPath := filepath.Join(tmpDir, "test.sock") socketPath := newTestSocketPath(t)
store, err := sqlite.New(context.Background(), dbPath) store, err := sqlite.New(context.Background(), dbPath)
if err != nil { if err != nil {
@@ -262,7 +260,7 @@ func TestStatusEndpointDefaultConfig(t *testing.T) {
func TestSetConfigConcurrency(t *testing.T) { func TestSetConfigConcurrency(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db") dbPath := filepath.Join(tmpDir, "test.db")
socketPath := filepath.Join(tmpDir, "test.sock") socketPath := newTestSocketPath(t)
store, err := sqlite.New(context.Background(), dbPath) store, err := sqlite.New(context.Background(), dbPath)
if err != nil { if err != nil {

View File

@@ -2,6 +2,9 @@ package rpc
import ( import (
"context" "context"
"os"
"path/filepath"
"runtime"
"testing" "testing"
"github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/storage/sqlite"
@@ -23,6 +26,22 @@ func newTestStore(t *testing.T, dbPath string) *sqlite.SQLiteStorage {
_ = store.Close() _ = store.Close()
t.Fatalf("Failed to set issue_prefix: %v", err) t.Fatalf("Failed to set issue_prefix: %v", err)
} }
return store 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")
}