From 6a509a53716aecddcbde869caa6fb942ba4c5b45 Mon Sep 17 00:00:00 2001 From: Bob Lail Date: Mon, 20 Jul 2026 14:46:06 -0700 Subject: [PATCH] fix: Skip resolving `Subcommands()` if there are no more arguments to identify --- identification.go | 5 +++++ identification_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/identification.go b/identification.go index c863d1f..36324d4 100644 --- a/identification.go +++ b/identification.go @@ -66,6 +66,11 @@ func identify(cmd Command, args []string) (Command, []string, error) { return def, args, nil } return nullCommand{parent: cmd, name: name}, rest, nil + } else if len(rest) == 0 { + // With no remaining args, there is nothing to descend into, so we don't + // resolve found's subcommands. This lets callers like `which` identify + // a command even when it doesn't fulfill a discovery contract. + return found, rest, nil } else if subcmds, err := found.Subcommands(); err != nil { return found, rest, err } else if len(subcmds) > 0 { diff --git a/identification_test.go b/identification_test.go index d76d87f..4842730 100644 --- a/identification_test.go +++ b/identification_test.go @@ -1,6 +1,7 @@ package exoskeleton import ( + "errors" "fmt" "testing" @@ -43,6 +44,42 @@ func TestIdentifyByAlias(t *testing.T) { } } +// A command discovered via a describe-based contract (e.g. OpenCLI) resolves +// its subcommands lazily by shelling out. When the target doesn't implement the +// describe flag, that resolution fails. Identifying such a command with no +// remaining args must not trigger the resolution — locating the command should +// not require running its describe flag. +func TestIdentifyDoesNotResolveSubcommandsWithoutRemainingArgs(t *testing.T) { + described := false + failing := &executableCommand{ + name: "plain", + discoverer: &discoverer{}, + cache: nullCache{}, + describe: func(cmd *executableCommand) (*commandDescriptor, error) { + described = true + return nil, errors.New("plain: error: unknown flag --help-opencli") + }, + } + + help := &builtinCommand{definition: &EmbeddedCommand{Name: `help`}} + complete := &builtinCommand{definition: &EmbeddedCommand{Name: `complete`}} + entrypoint := &Entrypoint{cmds: Commands{help, complete, failing}} + + // No remaining args: subcommands must not be resolved. + cmd, rest, err := entrypoint.Identify([]string{"plain"}) + assert.NoError(t, err) + assert.Equal(t, failing, cmd) + assert.Equal(t, []string{}, rest) + assert.False(t, described, "should not have attempted to describe the command") + + // With a remaining positional arg, subcommands are resolved to look for a + // match — confirming the short-circuit is specific to the empty-rest case. + described = false + _, _, err = entrypoint.Identify([]string{"plain", "sub"}) + assert.Error(t, err) + assert.True(t, described, "should have attempted to describe the command") +} + func TestIdentify(t *testing.T) { // all // ├── a