From 8de8f8e5cc7d805afd61b4dd17c5d8b1673fcb51 Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 17 Jun 2026 03:58:50 +0200 Subject: [PATCH] Strip fx call-chain noise from startup errors; clarify file:// error --- internal/cli/app.go | 21 ++++++++++++++++++++- internal/cli/app_test.go | 39 +++++++++++++++++++++++++++++++++++++++ internal/storage/file.go | 3 +-- 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 internal/cli/app_test.go diff --git a/internal/cli/app.go b/internal/cli/app.go index a595f6f..389507a 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -7,6 +7,7 @@ import ( "os" "os/signal" "path/filepath" + "strings" "syscall" "time" @@ -68,6 +69,24 @@ func NewApp(opts AppOptions) *fx.App { return fx.New(allOptions...) } +// cleanStartupError strips fx's dependency-injection call-chain noise from +// startup errors. fx wraps the underlying error with messages like +// +// could not build arguments for function "X" (file:line): failed to build T: +// could not build arguments for function "Y" (file:line): failed to build U: +// received non-nil error from function "Z" (file:line): +// +// Users care about the real error, not the DI plumbing. We strip everything +// up through the last "): " (which is always the close-paren of an fx +// function-location annotation followed by the wrapped error). +func cleanStartupError(err error) error { + msg := err.Error() + if idx := strings.LastIndex(msg, "): "); idx >= 0 { + msg = msg[idx+3:] + } + return errors.New(msg) +} + // RunApp starts and stops the fx application within the given context. // It handles graceful shutdown on interrupt signals (SIGINT, SIGTERM) and // ensures the application stops cleanly. The function blocks until the @@ -83,7 +102,7 @@ func RunApp(ctx context.Context, app *fx.App) error { // Start the app if err := app.Start(ctx); err != nil { - return fmt.Errorf("failed to start app: %w", err) + return cleanStartupError(err) } // Handle shutdown diff --git a/internal/cli/app_test.go b/internal/cli/app_test.go new file mode 100644 index 0000000..1d90476 --- /dev/null +++ b/internal/cli/app_test.go @@ -0,0 +1,39 @@ +package cli + +import ( + "errors" + "testing" +) + +func TestCleanStartupError(t *testing.T) { + tests := []struct { + name string + in string + want string + }{ + { + name: "real fx error chain", + in: `could not build arguments for function "sneak.berlin/go/vaultik/internal/cli".newSnapshotCreateCommand.func1.1 (/Users/user/dev/vaultik/internal/cli/snapshot.go:71): failed to build *vaultik.Vaultik: could not build arguments for function "sneak.berlin/go/vaultik/internal/vaultik".New (/Users/user/dev/vaultik/internal/vaultik/vaultik.go:59): failed to build storage.Storer: received non-nil error from function "sneak.berlin/go/vaultik/internal/storage".NewStorer (/Users/user/dev/vaultik/internal/storage/module.go:23): creating base path: mkdir /Volumes/BACKUPS: permission denied`, + want: `creating base path: mkdir /Volumes/BACKUPS: permission denied`, + }, + { + name: "no fx wrapping", + in: "plain error", + want: "plain error", + }, + { + name: "single fx wrapping", + in: `received non-nil error from function "foo" (file.go:1): underlying problem`, + want: "underlying problem", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := cleanStartupError(errors.New(tt.in)).Error() + if got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} diff --git a/internal/storage/file.go b/internal/storage/file.go index a5b3ae8..deb8c0a 100644 --- a/internal/storage/file.go +++ b/internal/storage/file.go @@ -23,9 +23,8 @@ type FileStorer struct { // Uses the real OS filesystem by default; call SetFilesystem to override for testing. func NewFileStorer(basePath string) (*FileStorer, error) { fs := afero.NewOsFs() - // Ensure base path exists if err := fs.MkdirAll(basePath, 0755); err != nil { - return nil, fmt.Errorf("creating base path: %w", err) + return nil, fmt.Errorf("file:// storage: cannot create or access %s: %w (check that the volume is mounted and writable)", basePath, err) } return &FileStorer{ fs: fs,