From 3e527cf6bbb17f2dd349567286928aef241d5b9c Mon Sep 17 00:00:00 2001 From: Tian Pan Date: Thu, 29 Oct 2026 17:46:10 -0700 Subject: [PATCH] fix: show wrapped error detail when a user-facing message is empty --- pkg/tui/error.go | 3 ++ pkg/tui/error_test.go | 81 +++++++++++++++++++++++++++++++++ pkg/tui/views/execerror_test.go | 27 +++++++++++ 3 files changed, 111 insertions(+) create mode 100644 pkg/tui/error_test.go create mode 100644 pkg/tui/views/execerror_test.go diff --git a/pkg/tui/error.go b/pkg/tui/error.go index 15865d26..e596f966 100644 --- a/pkg/tui/error.go +++ b/pkg/tui/error.go @@ -57,6 +57,9 @@ func (m *ErrorModel) View() string { if errors.As(m.Err, userFacingError) { title = userFacingError.Title message = userFacingError.Message + if message == "" && userFacingError.Err != nil { + message = userFacingError.Err.Error() + } } else { message = m.Err.Error() } diff --git a/pkg/tui/error_test.go b/pkg/tui/error_test.go new file mode 100644 index 00000000..36debc2c --- /dev/null +++ b/pkg/tui/error_test.go @@ -0,0 +1,81 @@ +package tui_test + +import ( + "errors" + "fmt" + "os/exec" + "testing" + + "github.com/render-oss/cli/pkg/tui" + "github.com/stretchr/testify/require" +) + +func renderErrorView(err error) string { + m := tui.NewErrorModel(err) + m.Update(tui.StackSizeMsg{Width: 120, Height: 20}) + return m.View() +} + +func TestErrorModelView(t *testing.T) { + t.Run("wrapped user facing error without message shows underlying error", func(t *testing.T) { + view := renderErrorView(tui.UserFacingError{ + Title: "An error occurred while running kv-cli", + Err: errors.New("exit status 1"), + }) + require.Contains(t, view, "An error occurred while running kv-cli") + require.Contains(t, view, "exit status 1") + }) + + t.Run("child process exit error is shown", func(t *testing.T) { + falsePath, err := exec.LookPath("false") + if err != nil { + t.Skip("false not available") + } + runErr := exec.Command(falsePath).Run() + var exitErr *exec.ExitError + require.ErrorAs(t, runErr, &exitErr) + + view := renderErrorView(tui.UserFacingError{ + Title: "An error occurred while running psql", + Err: runErr, + }) + require.Contains(t, view, "An error occurred while running psql") + require.Contains(t, view, runErr.Error()) + }) + + t.Run("user facing error nested in another error shows underlying error", func(t *testing.T) { + view := renderErrorView(fmt.Errorf("context: %w", tui.UserFacingError{ + Title: "An error occurred while running pgcli", + Err: errors.New("exit status 2"), + })) + require.Contains(t, view, "An error occurred while running pgcli") + require.Contains(t, view, "exit status 2") + }) + + t.Run("explicit message takes precedence over underlying error", func(t *testing.T) { + view := renderErrorView(tui.UserFacingError{ + Title: "Failed to SSH", + Message: "explicit message", + Err: errors.New("hidden underlying error"), + }) + require.Contains(t, view, "Failed to SSH") + require.Contains(t, view, "explicit message") + require.NotContains(t, view, "hidden underlying error") + }) + + t.Run("title only error shows title", func(t *testing.T) { + view := renderErrorView(tui.UserFacingError{Title: "only a title"}) + require.Contains(t, view, "only a title") + require.NotContains(t, view, "unknown error") + }) + + t.Run("message only error shows message", func(t *testing.T) { + view := renderErrorView(tui.UserFacingError{Message: "No Key Value named 'x'."}) + require.Contains(t, view, "No Key Value named 'x'.") + }) + + t.Run("ordinary error shows error string", func(t *testing.T) { + view := renderErrorView(errors.New("plain failure")) + require.Contains(t, view, "plain failure") + }) +} diff --git a/pkg/tui/views/execerror_test.go b/pkg/tui/views/execerror_test.go new file mode 100644 index 00000000..54635f6b --- /dev/null +++ b/pkg/tui/views/execerror_test.go @@ -0,0 +1,27 @@ +package views + +import ( + "errors" + "testing" + + "github.com/render-oss/cli/pkg/tui" + "github.com/stretchr/testify/require" +) + +func TestDatastoreExecErrorsShowUnderlyingError(t *testing.T) { + childErr := errors.New("exit status 1") + + for name, wrapped := range map[string]error{ + "kv-cli": handleRedisCLIError(childErr), + "psql": handlePSQLError(PSQL)(childErr), + "pgcli": handlePSQLError(PGCLI)(childErr), + } { + t.Run(name, func(t *testing.T) { + m := tui.NewErrorModel(wrapped) + m.Update(tui.StackSizeMsg{Width: 120, Height: 20}) + view := m.View() + require.Contains(t, view, "An error occurred while running "+name) + require.Contains(t, view, "exit status 1") + }) + } +}