From f66e7e8dacfca50522c77072ec0a29450680e3c6 Mon Sep 17 00:00:00 2001 From: AJ Alon Date: Tue, 1 Sep 2026 10:53:37 -0700 Subject: [PATCH 1/4] [CFX-7958] fix(self): correct completion install/uninstall hints to include "self" Every user-facing hint in cmd/self/completion/install/cmd.go and cmd/self/completion/uninstall/cmd.go (Example blocks, the "already installed" reinstall hint, and the dry-run hints) told users to run `dr completion install/uninstall ...`, but those commands only exist under `dr self completion install/uninstall ...`. Copy-pasting the hint failed with an unknown-command error. install/cmd.go line 354 already had the correct `self` prefix; the rest did not. The two runtime-printed hints (reinstall and dry-run, in both files) now derive their command path from cmd.CommandPath() instead of a hardcoded string, so they can't silently drift out of sync again if the command is ever renamed or reparented. --- cmd/self/completion/install/cmd.go | 41 ++++++++++++++++------------ cmd/self/completion/uninstall/cmd.go | 22 +++++++++------ 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/cmd/self/completion/install/cmd.go b/cmd/self/completion/install/cmd.go index 90ca155dd..eb02651fa 100644 --- a/cmd/self/completion/install/cmd.go +++ b/cmd/self/completion/install/cmd.go @@ -60,20 +60,20 @@ This command will: By default, this command runs in preview mode. Use '--yes' to install directly.`, Example: ` # Preview what would be installed (default behavior): - ` + version.CliName + ` completion install + ` + version.CliName + ` self completion install # Install completions for your current shell: - ` + version.CliName + ` completion install --yes + ` + version.CliName + ` self completion install --yes # Install completions for a specific shell: - ` + version.CliName + ` completion install bash --yes - ` + version.CliName + ` completion install zsh --yes + ` + version.CliName + ` self completion install bash --yes + ` + version.CliName + ` self completion install zsh --yes # Preview installation for a specific shell: - ` + version.CliName + ` completion install bash + ` + version.CliName + ` self completion install bash # Force reinstall, even if completions are already installed: - ` + version.CliName + ` completion install --force --yes`, + ` + version.CliName + ` self completion install --force --yes`, Args: cobra.MaximumNArgs(1), ValidArgs: internalShell.SupportedShells(), RunE: func(cmd *cobra.Command, args []string) error { @@ -92,7 +92,7 @@ By default, this command runs in preview mode. Use '--yes' to install directly.` effectiveDryRun = true } - return runInstall(cmd.Root(), shell, force, yes, effectiveDryRun) + return runInstall(cmd, shell, force, yes, effectiveDryRun) }, } @@ -103,7 +103,12 @@ By default, this command runs in preview mode. Use '--yes' to install directly.` return cmd } -func runInstall(rootCmd *cobra.Command, specifiedShell string, force, yes, dryRun bool) error { +// runInstall takes the invoked *cobra.Command (not just its root) so that +// user-facing hints can derive the exact "dr self completion install" path +// from cmd.CommandPath() instead of a hardcoded string. A hardcoded literal +// silently drifts from the real command path if this command is ever +// renamed or reparented — see CFX-7958. +func runInstall(cmd *cobra.Command, specifiedShell string, force, yes, dryRun bool) error { shell, err := internalShell.ResolveShell(specifiedShell) if err != nil { return fmt.Errorf("resolve shell: %w", err) @@ -120,11 +125,11 @@ func runInstall(rootCmd *cobra.Command, specifiedShell string, force, yes, dryRu return nil } - return installForShell(rootCmd, shell, shellType, force, yes, dryRun) + return installForShell(cmd, shell, shellType, force, yes, dryRun) } -func installForShell(rootCmd *cobra.Command, shell string, shellType internalShell.Shell, force, yes, dryRun bool) error { - installPath, installFunc, err := getInstallFunc(rootCmd, shellType, force) +func installForShell(cmd *cobra.Command, shell string, shellType internalShell.Shell, force, yes, dryRun bool) error { + installPath, installFunc, err := getInstallFunc(cmd.Root(), shellType, force) if err != nil { return err } @@ -132,7 +137,7 @@ func installForShell(rootCmd *cobra.Command, shell string, shellType internalShe // Check if already installed alreadyInstalled := fsutil.FileExists(installPath) if !force && alreadyInstalled { - showAlreadyInstalled(installPath) + showAlreadyInstalled(cmd, installPath) return nil } @@ -141,7 +146,7 @@ func installForShell(rootCmd *cobra.Command, shell string, shellType internalShe // Dry-run mode if dryRun { - showDryRunMessage(shell) + showDryRunMessage(cmd, shell) return nil } @@ -160,7 +165,7 @@ func installForShell(rootCmd *cobra.Command, shell string, shellType internalShe fmt.Println() // Install - if err := installFunc(rootCmd); err != nil { + if err := installFunc(cmd.Root()); err != nil { return fmt.Errorf("Failed to install completions: %w", err) } @@ -173,10 +178,10 @@ func installForShell(rootCmd *cobra.Command, shell string, shellType internalShe return nil } -func showAlreadyInstalled(installPath string) { +func showAlreadyInstalled(cmd *cobra.Command, installPath string) { fmt.Printf("%s Completion already installed at: %s.\n", successStyle.Render("✓"), installPath) fmt.Println() - fmt.Println(infoStyle.Render("To reinstall, use: " + version.CliName + " completion install --force --yes")) + fmt.Println(infoStyle.Render("To reinstall, use: " + cmd.CommandPath() + " --force --yes")) } func showInstallationPlan(shell, installPath string, alreadyInstalled bool) { @@ -193,11 +198,11 @@ func showInstallationPlan(shell, installPath string, alreadyInstalled bool) { fmt.Println() } -func showDryRunMessage(shell string) { +func showDryRunMessage(cmd *cobra.Command, shell string) { fmt.Println(infoStyle.Render("🔍 Dry-run mode (no changes will be made)")) fmt.Println() fmt.Println("To proceed with installation, run:") - fmt.Println(infoStyle.Render(" " + version.CliName + " completion install " + shell + " --yes")) + fmt.Println(infoStyle.Render(" " + cmd.CommandPath() + " " + shell + " --yes")) } func promptForConfirmation() (bool, error) { diff --git a/cmd/self/completion/uninstall/cmd.go b/cmd/self/completion/uninstall/cmd.go index 46ac69cab..6b3a86651 100644 --- a/cmd/self/completion/uninstall/cmd.go +++ b/cmd/self/completion/uninstall/cmd.go @@ -53,14 +53,14 @@ This command will: By default, runs in preview mode. Use '--yes' to uninstall directly.`, Example: ` # Preview what would be removed (default behavior) - ` + version.CliName + ` completion uninstall + ` + version.CliName + ` self completion uninstall # Uninstall completions for your current shell - ` + version.CliName + ` completion uninstall --yes + ` + version.CliName + ` self completion uninstall --yes # Uninstall completions for a specific shell - ` + version.CliName + ` completion uninstall bash --yes - ` + version.CliName + ` completion uninstall zsh --yes`, + ` + version.CliName + ` self completion uninstall bash --yes + ` + version.CliName + ` self completion uninstall zsh --yes`, Args: cobra.MaximumNArgs(1), ValidArgs: internalShell.SupportedShells(), RunE: func(cmd *cobra.Command, args []string) error { @@ -79,7 +79,7 @@ By default, runs in preview mode. Use '--yes' to uninstall directly.`, effectiveDryRun = true } - return runUninstall(shell, yes, effectiveDryRun) + return runUninstall(cmd, shell, yes, effectiveDryRun) }, } @@ -89,7 +89,11 @@ By default, runs in preview mode. Use '--yes' to uninstall directly.`, return cmd } -func runUninstall(specifiedShell string, yes, dryRun bool) error { +// runUninstall takes the invoked *cobra.Command so the dry-run hint can +// derive the exact "dr self completion uninstall" path from +// cmd.CommandPath() instead of a hardcoded string — see CFX-7958, where a +// hardcoded literal silently drifted from the real command path. +func runUninstall(cmd *cobra.Command, specifiedShell string, yes, dryRun bool) error { shell, err := resolveShellForUninstall(specifiedShell) if err != nil { return err @@ -108,7 +112,7 @@ func runUninstall(specifiedShell string, yes, dryRun bool) error { // Dry-run mode if dryRun { - showUninstallDryRunMessage(shell) + showUninstallDryRunMessage(cmd, shell) return nil } @@ -172,11 +176,11 @@ func showUninstallationPlan(shell string, existingPaths []string) { fmt.Println() } -func showUninstallDryRunMessage(shell string) { +func showUninstallDryRunMessage(cmd *cobra.Command, shell string) { fmt.Println(infoStyle.Render("🔍 Dry-run mode (no changes will be made)")) fmt.Println() fmt.Println("To proceed with uninstallation, run:") - fmt.Println(infoStyle.Render(" " + version.CliName + " completion uninstall " + shell + " --yes")) + fmt.Println(infoStyle.Render(" " + cmd.CommandPath() + " " + shell + " --yes")) } func performUninstall(shell internalShell.Shell) error { From 4f19d5b0a4b533b61bcd3e2bebf02cb9265ca4bf Mon Sep 17 00:00:00 2001 From: AJ Alon Date: Tue, 1 Sep 2026 11:15:46 -0700 Subject: [PATCH 2/4] test(self): cover completion hint CommandPath() derivation --- cmd/self/completion/install/cmd_test.go | 87 +++++++++++++++++++++++ cmd/self/completion/uninstall/cmd_test.go | 67 +++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/cmd/self/completion/install/cmd_test.go b/cmd/self/completion/install/cmd_test.go index 586cf9682..c7f83c99d 100644 --- a/cmd/self/completion/install/cmd_test.go +++ b/cmd/self/completion/install/cmd_test.go @@ -16,6 +16,7 @@ package install import ( "fmt" + "io" "os" "path/filepath" "strings" @@ -23,9 +24,57 @@ import ( "github.com/datarobot/cli/internal/fsutil" internalShell "github.com/datarobot/cli/internal/shell" + "github.com/datarobot/cli/internal/version" "github.com/spf13/cobra" ) +// wireCommandPath attaches cmd to a synthetic "dr self completion" parent +// chain matching the real registration (see cmd/self/cmd.go and +// cmd/self/completion/cmd.go), so cmd.CommandPath() resolves exactly as it +// does at runtime. This package can't import cmd/self/completion directly — +// that package imports this one, so doing so would create an import cycle. +func wireCommandPath(cmd *cobra.Command) { + root := &cobra.Command{Use: version.CliName} + selfCmd := &cobra.Command{Use: "self"} + completionCmd := &cobra.Command{Use: "completion"} + + completionCmd.AddCommand(cmd) + selfCmd.AddCommand(completionCmd) + root.AddCommand(selfCmd) +} + +// captureStdout redirects os.Stdout for the duration of fn and returns +// everything written to it. +func captureStdout(t *testing.T, fn func()) string { + t.Helper() + + orig := os.Stdout + + t.Cleanup(func() { os.Stdout = orig }) + + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("create pipe: %v", err) + } + + os.Stdout = w + + fn() + + os.Stdout = orig + + if err := w.Close(); err != nil { + t.Fatalf("close pipe: %v", err) + } + + out, err := io.ReadAll(r) + if err != nil { + t.Fatalf("read pipe: %v", err) + } + + return string(out) +} + func TestDetectShell(t *testing.T) { // DetectShell() now prioritizes parent process detection over $SHELL. // In the test environment, the parent is the test runner (e.g., "go") @@ -261,6 +310,44 @@ func TestInstallCmd(t *testing.T) { } } +// TestShowAlreadyInstalled guards against CFX-7958: the printed reinstall +// hint must derive its command path from cmd.CommandPath() so it can never +// drift from the real "dr self completion install" invocation. +func TestShowAlreadyInstalled(t *testing.T) { + cmd := Cmd() + wireCommandPath(cmd) + + out := captureStdout(t, func() { + showAlreadyInstalled(cmd, "/fake/path/_dr") + }) + + if !strings.Contains(out, "/fake/path/_dr") { + t.Errorf("output %q does not mention the install path", out) + } + + wantHint := version.CliName + " self completion install --force --yes" + if !strings.Contains(out, wantHint) { + t.Errorf("output %q does not contain expected reinstall hint %q", out, wantHint) + } +} + +// TestShowDryRunMessage guards against CFX-7958: the printed dry-run hint +// must derive its command path from cmd.CommandPath() so it can never +// drift from the real "dr self completion install" invocation. +func TestShowDryRunMessage(t *testing.T) { + cmd := Cmd() + wireCommandPath(cmd) + + out := captureStdout(t, func() { + showDryRunMessage(cmd, "zsh") + }) + + wantHint := version.CliName + " self completion install zsh --yes" + if !strings.Contains(out, wantHint) { + t.Errorf("output %q does not contain expected dry-run hint %q", out, wantHint) + } +} + func TestIsBashCompletionAvailable(_ *testing.T) { // This test just ensures the function doesn't panic // The actual result depends on the system diff --git a/cmd/self/completion/uninstall/cmd_test.go b/cmd/self/completion/uninstall/cmd_test.go index 7118f4d5c..9cc22c354 100644 --- a/cmd/self/completion/uninstall/cmd_test.go +++ b/cmd/self/completion/uninstall/cmd_test.go @@ -15,6 +15,7 @@ package uninstall import ( + "io" "os" "path/filepath" "runtime" @@ -24,8 +25,57 @@ import ( "github.com/datarobot/cli/internal/fsutil" internalShell "github.com/datarobot/cli/internal/shell" "github.com/datarobot/cli/internal/testutil" + "github.com/datarobot/cli/internal/version" + "github.com/spf13/cobra" ) +// wireCommandPath attaches cmd to a synthetic "dr self completion" parent +// chain matching the real registration (see cmd/self/cmd.go and +// cmd/self/completion/cmd.go), so cmd.CommandPath() resolves exactly as it +// does at runtime. This package can't import cmd/self/completion directly — +// that package imports this one, so doing so would create an import cycle. +func wireCommandPath(cmd *cobra.Command) { + root := &cobra.Command{Use: version.CliName} + selfCmd := &cobra.Command{Use: "self"} + completionCmd := &cobra.Command{Use: "completion"} + + completionCmd.AddCommand(cmd) + selfCmd.AddCommand(completionCmd) + root.AddCommand(selfCmd) +} + +// captureStdout redirects os.Stdout for the duration of fn and returns +// everything written to it. +func captureStdout(t *testing.T, fn func()) string { + t.Helper() + + orig := os.Stdout + + t.Cleanup(func() { os.Stdout = orig }) + + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("create pipe: %v", err) + } + + os.Stdout = w + + fn() + + os.Stdout = orig + + if err := w.Close(); err != nil { + t.Fatalf("close pipe: %v", err) + } + + out, err := io.ReadAll(r) + if err != nil { + t.Fatalf("read pipe: %v", err) + } + + return string(out) +} + func TestFindExistingCompletions(t *testing.T) { // Create temporary directory structure tmpDir, err := os.MkdirTemp("", "test-completions-*") @@ -131,6 +181,23 @@ func TestUninstallCmd(t *testing.T) { } } +// TestShowUninstallDryRunMessage guards against CFX-7958: the printed +// dry-run hint must derive its command path from cmd.CommandPath() so it +// can never drift from the real "dr self completion uninstall" invocation. +func TestShowUninstallDryRunMessage(t *testing.T) { + cmd := Cmd() + wireCommandPath(cmd) + + out := captureStdout(t, func() { + showUninstallDryRunMessage(cmd, "zsh") + }) + + wantHint := version.CliName + " self completion uninstall zsh --yes" + if !strings.Contains(out, wantHint) { + t.Errorf("output %q does not contain expected dry-run hint %q", out, wantHint) + } +} + func TestGetUninstallPaths(t *testing.T) { testHome := "/test/home" testutil.SetTestHomeDir(t, testHome) From ac9a002e9ce921a66a758a81ef4a09aaf5350827 Mon Sep 17 00:00:00 2001 From: AJ Alon Date: Thu, 3 Sep 2026 09:28:59 -0700 Subject: [PATCH 3/4] test(completion): deduplicate command test helpers Move shared completion test helpers into internal/testutil and reuse them from install and uninstall tests. Trim repeated CommandPath rationale comments and reuse the existing test home helper in the PowerShell profile test. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- cmd/self/completion/install/cmd.go | 6 +- cmd/self/completion/install/cmd_test.go | 76 +++-------------------- cmd/self/completion/uninstall/cmd.go | 5 +- cmd/self/completion/uninstall/cmd_test.go | 57 +---------------- internal/testutil/command.go | 66 ++++++++++++++++++++ 5 files changed, 79 insertions(+), 131 deletions(-) create mode 100644 internal/testutil/command.go diff --git a/cmd/self/completion/install/cmd.go b/cmd/self/completion/install/cmd.go index eb02651fa..0bac45de9 100644 --- a/cmd/self/completion/install/cmd.go +++ b/cmd/self/completion/install/cmd.go @@ -103,11 +103,7 @@ By default, this command runs in preview mode. Use '--yes' to install directly.` return cmd } -// runInstall takes the invoked *cobra.Command (not just its root) so that -// user-facing hints can derive the exact "dr self completion install" path -// from cmd.CommandPath() instead of a hardcoded string. A hardcoded literal -// silently drifts from the real command path if this command is ever -// renamed or reparented — see CFX-7958. +// runInstall keeps user-facing hints tied to the invoked command path. func runInstall(cmd *cobra.Command, specifiedShell string, force, yes, dryRun bool) error { shell, err := internalShell.ResolveShell(specifiedShell) if err != nil { diff --git a/cmd/self/completion/install/cmd_test.go b/cmd/self/completion/install/cmd_test.go index c7f83c99d..c371e4220 100644 --- a/cmd/self/completion/install/cmd_test.go +++ b/cmd/self/completion/install/cmd_test.go @@ -16,7 +16,6 @@ package install import ( "fmt" - "io" "os" "path/filepath" "strings" @@ -24,57 +23,11 @@ import ( "github.com/datarobot/cli/internal/fsutil" internalShell "github.com/datarobot/cli/internal/shell" + "github.com/datarobot/cli/internal/testutil" "github.com/datarobot/cli/internal/version" "github.com/spf13/cobra" ) -// wireCommandPath attaches cmd to a synthetic "dr self completion" parent -// chain matching the real registration (see cmd/self/cmd.go and -// cmd/self/completion/cmd.go), so cmd.CommandPath() resolves exactly as it -// does at runtime. This package can't import cmd/self/completion directly — -// that package imports this one, so doing so would create an import cycle. -func wireCommandPath(cmd *cobra.Command) { - root := &cobra.Command{Use: version.CliName} - selfCmd := &cobra.Command{Use: "self"} - completionCmd := &cobra.Command{Use: "completion"} - - completionCmd.AddCommand(cmd) - selfCmd.AddCommand(completionCmd) - root.AddCommand(selfCmd) -} - -// captureStdout redirects os.Stdout for the duration of fn and returns -// everything written to it. -func captureStdout(t *testing.T, fn func()) string { - t.Helper() - - orig := os.Stdout - - t.Cleanup(func() { os.Stdout = orig }) - - r, w, err := os.Pipe() - if err != nil { - t.Fatalf("create pipe: %v", err) - } - - os.Stdout = w - - fn() - - os.Stdout = orig - - if err := w.Close(); err != nil { - t.Fatalf("close pipe: %v", err) - } - - out, err := io.ReadAll(r) - if err != nil { - t.Fatalf("read pipe: %v", err) - } - - return string(out) -} - func TestDetectShell(t *testing.T) { // DetectShell() now prioritizes parent process detection over $SHELL. // In the test environment, the parent is the test runner (e.g., "go") @@ -310,14 +263,12 @@ func TestInstallCmd(t *testing.T) { } } -// TestShowAlreadyInstalled guards against CFX-7958: the printed reinstall -// hint must derive its command path from cmd.CommandPath() so it can never -// drift from the real "dr self completion install" invocation. +// TestShowAlreadyInstalled guards against CFX-7958 command path drift. func TestShowAlreadyInstalled(t *testing.T) { cmd := Cmd() - wireCommandPath(cmd) + testutil.WireCompletionCommandPath(cmd) - out := captureStdout(t, func() { + out := testutil.CaptureStdout(t, func() { showAlreadyInstalled(cmd, "/fake/path/_dr") }) @@ -331,14 +282,12 @@ func TestShowAlreadyInstalled(t *testing.T) { } } -// TestShowDryRunMessage guards against CFX-7958: the printed dry-run hint -// must derive its command path from cmd.CommandPath() so it can never -// drift from the real "dr self completion install" invocation. +// TestShowDryRunMessage guards against CFX-7958 command path drift. func TestShowDryRunMessage(t *testing.T) { cmd := Cmd() - wireCommandPath(cmd) + testutil.WireCompletionCommandPath(cmd) - out := captureStdout(t, func() { + out := testutil.CaptureStdout(t, func() { showDryRunMessage(cmd, "zsh") }) @@ -592,16 +541,7 @@ func TestInstallPowerShell(t *testing.T) { } defer os.RemoveAll(tmpDir) - // On Windows, os.UserHomeDir() reads USERPROFILE, not HOME. - // Set both so the test isolates the profile path on all platforms. - origHome := os.Getenv("HOME") - origUserProfile := os.Getenv("USERPROFILE") - - os.Setenv("HOME", tmpDir) - os.Setenv("USERPROFILE", tmpDir) - - defer os.Setenv("HOME", origHome) - defer os.Setenv("USERPROFILE", origUserProfile) + testutil.SetTestHomeDir(t, tmpDir) profilePath, installFn := installPowerShell(rootCmd, false) diff --git a/cmd/self/completion/uninstall/cmd.go b/cmd/self/completion/uninstall/cmd.go index 6b3a86651..40e94bc36 100644 --- a/cmd/self/completion/uninstall/cmd.go +++ b/cmd/self/completion/uninstall/cmd.go @@ -89,10 +89,7 @@ By default, runs in preview mode. Use '--yes' to uninstall directly.`, return cmd } -// runUninstall takes the invoked *cobra.Command so the dry-run hint can -// derive the exact "dr self completion uninstall" path from -// cmd.CommandPath() instead of a hardcoded string — see CFX-7958, where a -// hardcoded literal silently drifted from the real command path. +// runUninstall keeps user-facing hints tied to the invoked command path. func runUninstall(cmd *cobra.Command, specifiedShell string, yes, dryRun bool) error { shell, err := resolveShellForUninstall(specifiedShell) if err != nil { diff --git a/cmd/self/completion/uninstall/cmd_test.go b/cmd/self/completion/uninstall/cmd_test.go index 9cc22c354..ff15f1eb5 100644 --- a/cmd/self/completion/uninstall/cmd_test.go +++ b/cmd/self/completion/uninstall/cmd_test.go @@ -15,7 +15,6 @@ package uninstall import ( - "io" "os" "path/filepath" "runtime" @@ -26,56 +25,8 @@ import ( internalShell "github.com/datarobot/cli/internal/shell" "github.com/datarobot/cli/internal/testutil" "github.com/datarobot/cli/internal/version" - "github.com/spf13/cobra" ) -// wireCommandPath attaches cmd to a synthetic "dr self completion" parent -// chain matching the real registration (see cmd/self/cmd.go and -// cmd/self/completion/cmd.go), so cmd.CommandPath() resolves exactly as it -// does at runtime. This package can't import cmd/self/completion directly — -// that package imports this one, so doing so would create an import cycle. -func wireCommandPath(cmd *cobra.Command) { - root := &cobra.Command{Use: version.CliName} - selfCmd := &cobra.Command{Use: "self"} - completionCmd := &cobra.Command{Use: "completion"} - - completionCmd.AddCommand(cmd) - selfCmd.AddCommand(completionCmd) - root.AddCommand(selfCmd) -} - -// captureStdout redirects os.Stdout for the duration of fn and returns -// everything written to it. -func captureStdout(t *testing.T, fn func()) string { - t.Helper() - - orig := os.Stdout - - t.Cleanup(func() { os.Stdout = orig }) - - r, w, err := os.Pipe() - if err != nil { - t.Fatalf("create pipe: %v", err) - } - - os.Stdout = w - - fn() - - os.Stdout = orig - - if err := w.Close(); err != nil { - t.Fatalf("close pipe: %v", err) - } - - out, err := io.ReadAll(r) - if err != nil { - t.Fatalf("read pipe: %v", err) - } - - return string(out) -} - func TestFindExistingCompletions(t *testing.T) { // Create temporary directory structure tmpDir, err := os.MkdirTemp("", "test-completions-*") @@ -181,14 +132,12 @@ func TestUninstallCmd(t *testing.T) { } } -// TestShowUninstallDryRunMessage guards against CFX-7958: the printed -// dry-run hint must derive its command path from cmd.CommandPath() so it -// can never drift from the real "dr self completion uninstall" invocation. +// TestShowUninstallDryRunMessage guards against CFX-7958 command path drift. func TestShowUninstallDryRunMessage(t *testing.T) { cmd := Cmd() - wireCommandPath(cmd) + testutil.WireCompletionCommandPath(cmd) - out := captureStdout(t, func() { + out := testutil.CaptureStdout(t, func() { showUninstallDryRunMessage(cmd, "zsh") }) diff --git a/internal/testutil/command.go b/internal/testutil/command.go new file mode 100644 index 000000000..4cb48aec5 --- /dev/null +++ b/internal/testutil/command.go @@ -0,0 +1,66 @@ +// Copyright 2026 DataRobot, Inc. and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutil + +import ( + "io" + "os" + "testing" + + "github.com/datarobot/cli/internal/version" + "github.com/spf13/cobra" +) + +// CaptureStdout redirects os.Stdout while fn runs and returns the captured text. +func CaptureStdout(t *testing.T, fn func()) string { + t.Helper() + + orig := os.Stdout + + t.Cleanup(func() { os.Stdout = orig }) + + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("create pipe: %v", err) + } + + os.Stdout = w + + fn() + + os.Stdout = orig + + if err := w.Close(); err != nil { + t.Fatalf("close pipe: %v", err) + } + + out, err := io.ReadAll(r) + if err != nil { + t.Fatalf("read pipe: %v", err) + } + + return string(out) +} + +// WireCompletionCommandPath attaches a command to the real completion parent path. +func WireCompletionCommandPath(cmd *cobra.Command) { + root := &cobra.Command{Use: version.CliName} + selfCmd := &cobra.Command{Use: "self"} + completionCmd := &cobra.Command{Use: "completion"} + + completionCmd.AddCommand(cmd) + selfCmd.AddCommand(completionCmd) + root.AddCommand(selfCmd) +} From 9f1c12dd65f1930ac74faa05d7880e80bdb04079 Mon Sep 17 00:00:00 2001 From: AJ Alon Date: Thu, 3 Sep 2026 09:46:40 -0700 Subject: [PATCH 4/4] fix(deps): update x crypto for smoke scan Bump golang.org/x/crypto to v0.55.0 to resolve the Trivy finding reported by the fork smoke workflow. go mod tidy also updates golang.org/x/net to v0.57.0. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 6dfdfc288..3ed224173 100644 --- a/go.mod +++ b/go.mod @@ -81,7 +81,7 @@ require ( github.com/yuin/goldmark v1.8.2 // indirect github.com/yuin/goldmark-emoji v1.0.6 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect - golang.org/x/crypto v0.53.0 // indirect + golang.org/x/crypto v0.55.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/net v0.56.0 // indirect + golang.org/x/net v0.57.0 // indirect ) diff --git a/go.sum b/go.sum index e925698eb..151289695 100644 --- a/go.sum +++ b/go.sum @@ -183,12 +183,12 @@ github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=