From 10b4a1f4272cdeb48e94cb290f7223589bff2cc6 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 19 Jan 2026 10:30:50 +0000 Subject: [PATCH] fix(root): avoid command name collision when registering extensions Signed-off-by: Babak K. Shandiz Co-authored-by: Kynan Ware Co-authored-by: Devraj Mehta --- pkg/cmd/root/extension_registration_test.go | 97 +++++++++++++++++++++ pkg/cmd/root/root.go | 7 ++ 2 files changed, 104 insertions(+) create mode 100644 pkg/cmd/root/extension_registration_test.go diff --git a/pkg/cmd/root/extension_registration_test.go b/pkg/cmd/root/extension_registration_test.go new file mode 100644 index 000000000..90b836e4a --- /dev/null +++ b/pkg/cmd/root/extension_registration_test.go @@ -0,0 +1,97 @@ +package root + +import ( + "testing" + + "github.com/cli/cli/v2/internal/browser" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/extensions" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewCmdRoot_ExtensionRegistration(t *testing.T) { + tests := []struct { + name string + extensions []string + wantRegistered []string + wantSkipped []string + }{ + { + name: "extension conflicts with core command 'copilot'", + extensions: []string{"copilot"}, + wantSkipped: []string{"copilot"}, + wantRegistered: []string{}, + }, + { + name: "extension does not conflict with any core command", + extensions: []string{"my-custom-extension"}, + wantSkipped: []string{}, + wantRegistered: []string{"my-custom-extension"}, + }, + { + name: "extension that conflicts with a core command's alias", + extensions: []string{"agent"}, + wantSkipped: []string{"agent"}, + wantRegistered: []string{}, + }, + { + name: "multiple extensions with some conflicts", + extensions: []string{"pr", "custom-ext", "issue", "another-ext"}, + wantSkipped: []string{"pr", "issue"}, + wantRegistered: []string{"custom-ext", "another-ext"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + + var extMocks []extensions.Extension + for _, extName := range tt.extensions { + extMocks = append(extMocks, &extensions.ExtensionMock{ + NameFunc: func() string { + return extName + }, + }) + } + + em := &extensions.ExtensionManagerMock{ + ListFunc: func() []extensions.Extension { + return extMocks + }, + } + + f := &cmdutil.Factory{ + IOStreams: ios, + Config: func() (gh.Config, error) { + return config.NewBlankConfig(), nil + }, + Browser: &browser.Stub{}, + ExtensionManager: em, + } + + cmd, err := NewCmdRoot(f, "", "") + require.NoError(t, err) + + // Verify skipped extensions (should find core command registered, not extension) + for _, extName := range tt.wantSkipped { + foundCmd, _, findErr := cmd.Find([]string{extName}) + assert.NoError(t, findErr, "command %q should be found", extName) + assert.NotNil(t, foundCmd, "command %q should exist", extName) + assert.NotEqual(t, "extension", foundCmd.GroupID, "command %q should be core command, not extension", extName) + } + + // Verify registered extensions (should find extension command registered) + for _, extName := range tt.wantRegistered { + foundCmd, _, findErr := cmd.Find([]string{extName}) + assert.NoError(t, findErr, "extension %q should be found", extName) + assert.NotNil(t, foundCmd, "extension %q should exist", extName) + assert.Equal(t, "extension", foundCmd.GroupID, "command %q should be extension command", extName) + } + }) + } +} diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 0fe451e0f..de359bd3e 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -183,6 +183,13 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command, em := f.ExtensionManager for _, e := range em.List() { extensionCmd := NewCmdExtension(io, em, e, nil) + // Don't register an extension command if it would + // conflict with a core command. + _, _, err := cmd.Find([]string{extensionCmd.Name()}) + if err == nil { + continue + } + cmd.AddCommand(extensionCmd) }