From b3be6ba79c98a7729765b8ef8d88c61f9ac8054f Mon Sep 17 00:00:00 2001 From: Tian Pan Date: Thu, 29 Oct 2026 17:46:23 -0700 Subject: [PATCH] fix: let an explicit output flag override an invalid output default --- cmd/root.go | 11 ++++-- cmd/workspaces_test.go | 83 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 0c1c5985..d14f7256 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -289,9 +289,14 @@ func rootPersistentPreRun(deps *dependencies.Dependencies) func(cmd *cobra.Comma } explicitOutputSet := cmd.Flags().Changed("output") - signals, err := deps.DetectRuntimeSignals() - if err != nil { - return printRootPreRunError(cmd, err) + // An explicit --output wins over RENDER_OUTPUT, so the environment is + // only read (and validated) when it would actually select the format. + var signals command.RuntimeSignals + if !explicitOutputSet { + signals, err = deps.DetectRuntimeSignals() + if err != nil { + return printRootPreRunError(cmd, err) + } } output, err := command.ResolveAutoOutput(explicitOutputSet, requestedOutput, signals) if err != nil { diff --git a/cmd/workspaces_test.go b/cmd/workspaces_test.go index d958d308..cbf5e387 100644 --- a/cmd/workspaces_test.go +++ b/cmd/workspaces_test.go @@ -66,3 +66,86 @@ func TestWorkspaces_NonInteractive_EmptyList(t *testing.T) { assert.NotContains(t, result.Stdout, "@", "expected no workspace rows — all workspace emails contain @") assert.True(t, server.HasRequest("GET", "/owners"), "expected GET /owners to be called") } + +// TestWorkspaces_ExplicitOutputOverridesInvalidRenderOutput runs the real root +// setup with the real runtime-signal detector, so RENDER_OUTPUT is read from the +// environment exactly as in a shipped binary. +func TestWorkspaces_ExplicitOutputOverridesInvalidRenderOutput(t *testing.T) { + testCases := []struct { + name string + args []string + wantErr string + wantRequest bool + wantStdout string + }{ + { + name: "explicit json wins over the invalid default", + args: []string{"workspaces", "--output", "json"}, + wantRequest: true, + wantStdout: `"name": "acme-corp"`, + }, + { + name: "explicit yaml wins over the invalid default", + args: []string{"workspaces", "-o", "yaml"}, + wantRequest: true, + wantStdout: "name: acme-corp", + }, + { + name: "explicit text wins over the invalid default", + args: []string{"workspaces", "-o", "text"}, + wantRequest: true, + wantStdout: "acme-corp", + }, + { + name: "invalid default is still refused when selected", + args: []string{"workspaces"}, + wantErr: "invalid RENDER_OUTPUT value: qa-invalid-output", + }, + { + name: "invalid explicit flag is still refused", + args: []string{"workspaces", "-o", "xml"}, + wantErr: "invalid output format: xml", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + server := renderapi.NewServer(t) + server.Owners.Add(renderapi.NewOwner(client.Owner{Name: "acme-corp", Email: "admin@acme.com"})) + t.Setenv("RENDER_CLI_CONFIG_PATH", newTestConfigPath(t)) + t.Setenv("RENDER_HOST", server.URL()) + t.Setenv("RENDER_API_KEY", "test-api-key") + t.Setenv("RENDER_WORKSPACE", "") + t.Setenv("RENDER_OUTPUT", "qa-invalid-output") + + c, err := client.NewClientWithResponses(server.URL()) + require.NoError(t, err) + deps := dependencies.New(c) + + root := newRootCmd() + // A fresh copy keeps an earlier case's merged --output state from + // leaking into this one. + cmd := *workspacesCmd + cmd.ResetFlags() + root.AddCommand(&cmd) + setupRootCmdPersistentRun(root, deps) + + var stdout, stderr bytes.Buffer + root.SetOut(&stdout) + root.SetErr(&stderr) + root.SetArgs(tc.args) + + execErr := root.Execute() + + if tc.wantErr != "" { + require.EqualError(t, execErr, tc.wantErr) + assert.Contains(t, stderr.String(), tc.wantErr) + assert.Empty(t, stdout.String()) + } else { + require.NoError(t, execErr) + assert.Contains(t, stdout.String(), tc.wantStdout) + } + assert.Equal(t, tc.wantRequest, server.HasRequest("GET", "/owners")) + }) + } +}