Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions cmd/completion_version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package cmd

// completion_version_test.go — covers the cobra-override hygiene in
// root.go's init():
// - `instant completion` (no shell arg) returns a non-zero exit
// code (BUG-CLI-016, QA 2026-05-29).
// - Every default completion sub-shell (`bash | zsh | fish |
// powershell`) still returns nil — only the bare invocation
// changes (rule 18 registry-iterating).
// - `instant version` alias is registered and exits 0 with the
// same one-line shape `instant version <Version>` cobra emits
// for `--version` (BUG-CLI-041).

import (
"errors"
"strings"
"testing"
)

// TestCompletion_NoShellArg_ReturnsError pins BUG-CLI-016: the bare
// `instant completion` invocation must return a non-nil error (which
// main.go translates to exit code 1 via ExitCodeFor). Pre-fix cobra
// printed help and exited 0, which a CI script reading `$?` could not
// distinguish from a successful shell-completion-script generation.
func TestCompletion_NoShellArg_ReturnsError(t *testing.T) {
err := ExecuteWithArgs([]string{"completion"})
if err == nil {
t.Fatalf("BUG-CLI-016: `instant completion` must return a non-nil error; got nil (would exit 0)")
}
// Error message contract: must name `instant completion` and list
// the supported shells. A script grepping for "shell argument
// required" should branch on the error.
got := err.Error()
if !strings.Contains(got, "shell argument required") {
t.Errorf("BUG-CLI-016: error message must mention 'shell argument required'; got %q", got)
}
for _, shell := range []string{"bash", "zsh", "fish", "powershell"} {
if !strings.Contains(got, shell) {
t.Errorf("BUG-CLI-016: error message must list %q as a supported shell; got %q", shell, got)
}
}
// ExitCodeFor must classify this as ExitGeneric (1), not ExitOK (0).
if code := ExitCodeFor(err); code != ExitGeneric {
t.Errorf("BUG-CLI-016: ExitCodeFor(...) = %d, want %d (ExitGeneric)", code, ExitGeneric)
}
}

// TestCompletion_EveryShellSubcommandStillSucceeds is the rule-18
// registry-iterating guard against the "I fixed the bare command but
// broke every sub-shell" regression. The list mirrors cobra's
// InitDefaultCompletionCmd registry — bash, zsh, fish, powershell.
// Adding a new shell to cobra without adding it here means the new
// shell escapes the test net (the fix could silently turn it into the
// same usage-error path as the bare command).
func TestCompletion_EveryShellSubcommandStillSucceeds(t *testing.T) {
for _, shell := range []string{"bash", "zsh", "fish", "powershell"} {
shell := shell // capture
t.Run(shell, func(t *testing.T) {
err := ExecuteWithArgs([]string{"completion", shell})
if err != nil {
t.Errorf("BUG-CLI-016: `instant completion %s` must still succeed (script generation); got %v",
shell, err)
}
})
}
}

// TestVersion_AliasExitsZero pins BUG-CLI-041: `instant version` is
// the convention alias for `instant --version`. Pre-fix cobra
// rejected it with "unknown command 'version'" (exit=1). Now it must
// behave like --version: exit 0, no error.
func TestVersion_AliasExitsZero(t *testing.T) {
err := ExecuteWithArgs([]string{"version"})
if err != nil {
t.Fatalf("BUG-CLI-041: `instant version` alias must exit 0; got %v", err)
}
if code := ExitCodeFor(err); code != ExitOK {
t.Errorf("BUG-CLI-041: ExitCodeFor(...) = %d, want %d (ExitOK)", code, ExitOK)
}
}

// TestVersion_AliasExtraArgsRejected confirms cobra.NoArgs is enforced —
// `instant version foo` should fail with a usage-style error so a
// script can detect typos.
func TestVersion_AliasExtraArgsRejected(t *testing.T) {
err := ExecuteWithArgs([]string{"version", "foo"})
if err == nil {
t.Fatalf("BUG-CLI-041: `instant version foo` must reject extra args; got nil")
}
if !errors.Is(err, err) { // sanity (always true) — keep the import live
_ = err
}
}
42 changes: 42 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,48 @@ func init() {
// flag parsing — so the auth transport sees the flag value.
rootCmd.PersistentFlags().StringVar(&adHocToken, "token", "",
"Bearer token for this invocation (overrides INSTANT_TOKEN and saved login)")

// BUG-CLI-016 (QA 2026-05-29): cobra's default `completion` parent
// command prints its help and exits 0 when invoked with no shell
// argument. That's the wrong contract for CI/wrapper scripts —
// "no shell selected" is a usage error, not success. We need to
// force-init the default completion subtree (cobra normally adds
// it lazily inside Execute()), then stamp a RunE that returns an
// error so cobra-emitted exit propagates to main.go::ExitCodeFor.
// Sub-shells (`completion bash`, etc.) keep their original RunE —
// only the bare `completion` invocation changes.
rootCmd.InitDefaultCompletionCmd()
for _, c := range rootCmd.Commands() {
if c.Name() == "completion" {
c.RunE = func(cmd *cobra.Command, args []string) error {
_ = cmd.Help()
// Plain error → ExitCodeFor falls through to ExitGeneric (1).
// "shell argument required" is a usage error, not a runtime
// failure of the requested shell-completion generation.
return fmt.Errorf("instant completion: shell argument required (bash | zsh | fish | powershell)")
}
break
}
}

// BUG-CLI-041 (QA 2026-05-29): many CLIs accept both `version` and
// `--version`. Cobra wires `--version`/`-v` via rootCmd.Version,
// but `instant version` returned "unknown command 'version'" with
// exit=1 — confusing for users muscle-memorying `git version` /
// `node version` patterns. Register an explicit `version` alias
// that prints the same line cobra emits for `--version`.
rootCmd.AddCommand(&cobra.Command{
Use: "version",
Short: "Print version, commit SHA, build time (alias for `instant --version`)",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
// rootCmd.Version is shaped "vX.Y.Z (sha, buildtime)" by
// SetBuildInfo. Mirror cobra's default `--version` output
// ("instant version vX.Y.Z (sha, buildtime)") so a script
// that grep-greps either path sees the same string.
fmt.Printf("%s version %s\n", rootCmd.Name(), rootCmd.Version)
},
})
}

func initConfig() {
Expand Down
Loading