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
5 changes: 4 additions & 1 deletion internal/cli/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,10 @@ func newAlertGetCmd() *cobra.Command {
Use: "get <alert_id>",
Short: "Get alert detail",
Long: curatedLong("Get the full detail of a single alert by ID.", "Alerts", "ReadInfo"),
Args: requireArgs("alert_id"),
// incident exposes the same lookup as both "get" and "detail"; accept the
// "detail" spelling here too so the two resources behave identically.
Aliases: []string{"detail"},
Args: requireArgs("alert_id"),
RunE: func(cmd *cobra.Command, args []string) error {
return runCommand(cmd, args, func(ctx *RunContext) error {
result, _, err := ctx.Client.Alerts.ReadInfo(cmdContext(ctx.Cmd), &flashduty.AlertInfoRequest{
Expand Down
109 changes: 109 additions & 0 deletions internal/cli/verb_aliases_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package cli

import (
"strings"
"testing"
)

// ---------------------------------------------------------------------------
// get/detail verb aliases
//
// The single-record read verb is spelled inconsistently across resources:
// incident answers both "get" and "detail", alert answered only "get", and
// channel answered only the path-derived "info". Aliases make the other
// spellings resolve to the same command instead of failing with
// "unknown command".
// ---------------------------------------------------------------------------

// TestCommandAlertDetailAliasResolvesToGet pins that `alert detail <id>` runs
// the same request as `alert get <id>` (POST /alert/info with the alert_id).
func TestCommandAlertDetailAliasResolvesToGet(t *testing.T) {
for _, verb := range []string{"get", "detail"} {
t.Run(verb, func(t *testing.T) {
saveAndResetGlobals(t)
stub := newGFStub(t)

if _, err := execCommand("alert", verb, "alert-1"); err != nil {
t.Fatalf("execCommand: %v", err)
}
if stub.lastPath != "/alert/info" {
t.Fatalf("expected /alert/info, got %q", stub.lastPath)
}
if stub.lastBody["alert_id"] != "alert-1" {
t.Fatalf("expected alert_id %q, got %#v", "alert-1", stub.lastBody["alert_id"])
}
})
}
}

// TestCommandChannelGetDetailAliasesResolveToInfo pins that `channel get <id>`
// and `channel detail <id>` run the same request as the canonical
// `channel info <id>` (POST /channel/info with the channel_id).
func TestCommandChannelGetDetailAliasesResolveToInfo(t *testing.T) {
for _, verb := range []string{"info", "get", "detail"} {
t.Run(verb, func(t *testing.T) {
saveAndResetGlobals(t)
stub := newGFStub(t)

if _, err := execCommand("channel", verb, "1001"); err != nil {
t.Fatalf("execCommand: %v", err)
}
if stub.lastPath != "/channel/info" {
t.Fatalf("expected /channel/info, got %q", stub.lastPath)
}
if stub.lastBody["channel_id"] != float64(1001) {
t.Fatalf("expected channel_id 1001, got %#v", stub.lastBody["channel_id"])
}
})
}
}

// TestCommandAliasHelpShowsCanonicalCommand verifies that invoking an alias
// with --help renders the aliased command's help (its canonical Use line and
// flags), not an error.
func TestCommandAliasHelpShowsCanonicalCommand(t *testing.T) {
saveAndResetGlobals(t)

out, err := execCommand("channel", "get", "--help")
if err != nil {
t.Fatalf("unexpected error running alias with --help: %v", err)
}
if !strings.Contains(out, "Get channel detail") {
t.Fatalf("expected the channel info command's help, got %q", out)
}
if !strings.Contains(out, "--channel-id") {
t.Fatalf("expected the channel info flags in help output, got %q", out)
}
}

// TestCommandUnknownVerbStillFailsLoudly guards the aliases against swallowing
// genuinely unknown verbs: they must keep the hard failure (and the
// did-you-mean suggestion for near misses) from newGroupCmd.
func TestCommandUnknownVerbStillFailsLoudly(t *testing.T) {
saveAndResetGlobals(t)

if _, err := execCommand("alert", "show", "alert-1"); err == nil {
t.Fatal("expected an error for unknown subcommand \"show\", got nil")
} else if !strings.Contains(err.Error(), `unknown command "show" for "flashduty alert"`) {
t.Fatalf("expected error to identify the unknown command, got %q", err.Error())
}

if _, err := execCommand("channel", "show", "1001"); err == nil {
t.Fatal("expected an error for unknown subcommand \"show\", got nil")
} else if !strings.Contains(err.Error(), `unknown command "show" for "flashduty channel"`) {
t.Fatalf("expected error to identify the unknown command, got %q", err.Error())
}

// A near-miss typo of a real verb still gets a suggestion. (Suggestions
// match command names; "gt" is a 1-edit typo of "get".)
_, err := execCommand("alert", "gt", "alert-1")
if err == nil {
t.Fatal("expected an error for unknown subcommand \"gt\", got nil")
}
if !strings.Contains(err.Error(), "Did you mean this?") {
t.Fatalf("expected a suggestion block, got %q", err.Error())
}
if !strings.Contains(err.Error(), "get") {
t.Fatalf("expected \"get\" to be suggested, got %q", err.Error())
}
}
1 change: 1 addition & 0 deletions internal/cli/zz_generated_channels.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions internal/cmd/cligen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,16 @@ var optionalPositional = map[string]bool{
"incidentInfo": true,
}

// opAliases maps an operationId to extra cobra aliases its generated command
// should accept, keyed like positionalOverride. Generated verbs are path-derived
// (channelInfo → "info"), which diverges from the get/detail spelling curated
// commands use for the same single-record read (incident get/detail, alert
// get). Aliases let the generated command answer both spellings so a slip
// (`channel get <id>`) succeeds instead of erroring.
var opAliases = map[string][]string{
"channelInfo": {"get", "detail"},
}

// positional describes the positional argument a generated command exposes.
type positional struct {
Wire string // request-body wire key the positional folds into
Expand Down Expand Up @@ -1042,6 +1052,13 @@ func emitCmd(fn string, s service, o specOp, mi methodInfo) string {
fmt.Fprintf(&b, "\t\tUse: %q,\n", use)
fmt.Fprintf(&b, "\t\tShort: %q,\n", oneLine(o.Summary))
fmt.Fprintf(&b, "\t\tLong: %s,\n", quoteMultiline(longHelp(o, scalars, complexFields, specByWire)))
if aliases := opAliases[o.OpID]; len(aliases) > 0 {
quoted := make([]string, len(aliases))
for i, a := range aliases {
quoted[i] = fmt.Sprintf("%q", a)
}
fmt.Fprintf(&b, "\t\tAliases: []string{%s},\n", strings.Join(quoted, ", "))
}
if hasPos {
// Required positionals use body-aware validators: the body field can come
// from a positional, its typed flag, or --data. Scalar positionals still
Expand Down