Skip to content

Commit 35ba373

Browse files
authored
Merge pull request #127 from flashcatcloud/fix/get-detail-verb-aliases
fix(cli): alias alert/channel single-record reads to the get/detail spellings
2 parents 9b6dfe9 + cf1bb53 commit 35ba373

4 files changed

Lines changed: 131 additions & 1 deletion

File tree

internal/cli/alert.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,10 @@ func newAlertGetCmd() *cobra.Command {
123123
Use: "get <alert_id>",
124124
Short: "Get alert detail",
125125
Long: curatedLong("Get the full detail of a single alert by ID.", "Alerts", "ReadInfo"),
126-
Args: requireArgs("alert_id"),
126+
// incident exposes the same lookup as both "get" and "detail"; accept the
127+
// "detail" spelling here too so the two resources behave identically.
128+
Aliases: []string{"detail"},
129+
Args: requireArgs("alert_id"),
127130
RunE: func(cmd *cobra.Command, args []string) error {
128131
return runCommand(cmd, args, func(ctx *RunContext) error {
129132
result, _, err := ctx.Client.Alerts.ReadInfo(cmdContext(ctx.Cmd), &flashduty.AlertInfoRequest{

internal/cli/verb_aliases_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package cli
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
// ---------------------------------------------------------------------------
9+
// get/detail verb aliases
10+
//
11+
// The single-record read verb is spelled inconsistently across resources:
12+
// incident answers both "get" and "detail", alert answered only "get", and
13+
// channel answered only the path-derived "info". Aliases make the other
14+
// spellings resolve to the same command instead of failing with
15+
// "unknown command".
16+
// ---------------------------------------------------------------------------
17+
18+
// TestCommandAlertDetailAliasResolvesToGet pins that `alert detail <id>` runs
19+
// the same request as `alert get <id>` (POST /alert/info with the alert_id).
20+
func TestCommandAlertDetailAliasResolvesToGet(t *testing.T) {
21+
for _, verb := range []string{"get", "detail"} {
22+
t.Run(verb, func(t *testing.T) {
23+
saveAndResetGlobals(t)
24+
stub := newGFStub(t)
25+
26+
if _, err := execCommand("alert", verb, "alert-1"); err != nil {
27+
t.Fatalf("execCommand: %v", err)
28+
}
29+
if stub.lastPath != "/alert/info" {
30+
t.Fatalf("expected /alert/info, got %q", stub.lastPath)
31+
}
32+
if stub.lastBody["alert_id"] != "alert-1" {
33+
t.Fatalf("expected alert_id %q, got %#v", "alert-1", stub.lastBody["alert_id"])
34+
}
35+
})
36+
}
37+
}
38+
39+
// TestCommandChannelGetDetailAliasesResolveToInfo pins that `channel get <id>`
40+
// and `channel detail <id>` run the same request as the canonical
41+
// `channel info <id>` (POST /channel/info with the channel_id).
42+
func TestCommandChannelGetDetailAliasesResolveToInfo(t *testing.T) {
43+
for _, verb := range []string{"info", "get", "detail"} {
44+
t.Run(verb, func(t *testing.T) {
45+
saveAndResetGlobals(t)
46+
stub := newGFStub(t)
47+
48+
if _, err := execCommand("channel", verb, "1001"); err != nil {
49+
t.Fatalf("execCommand: %v", err)
50+
}
51+
if stub.lastPath != "/channel/info" {
52+
t.Fatalf("expected /channel/info, got %q", stub.lastPath)
53+
}
54+
if stub.lastBody["channel_id"] != float64(1001) {
55+
t.Fatalf("expected channel_id 1001, got %#v", stub.lastBody["channel_id"])
56+
}
57+
})
58+
}
59+
}
60+
61+
// TestCommandAliasHelpShowsCanonicalCommand verifies that invoking an alias
62+
// with --help renders the aliased command's help (its canonical Use line and
63+
// flags), not an error.
64+
func TestCommandAliasHelpShowsCanonicalCommand(t *testing.T) {
65+
saveAndResetGlobals(t)
66+
67+
out, err := execCommand("channel", "get", "--help")
68+
if err != nil {
69+
t.Fatalf("unexpected error running alias with --help: %v", err)
70+
}
71+
if !strings.Contains(out, "Get channel detail") {
72+
t.Fatalf("expected the channel info command's help, got %q", out)
73+
}
74+
if !strings.Contains(out, "--channel-id") {
75+
t.Fatalf("expected the channel info flags in help output, got %q", out)
76+
}
77+
}
78+
79+
// TestCommandUnknownVerbStillFailsLoudly guards the aliases against swallowing
80+
// genuinely unknown verbs: they must keep the hard failure (and the
81+
// did-you-mean suggestion for near misses) from newGroupCmd.
82+
func TestCommandUnknownVerbStillFailsLoudly(t *testing.T) {
83+
saveAndResetGlobals(t)
84+
85+
if _, err := execCommand("alert", "show", "alert-1"); err == nil {
86+
t.Fatal("expected an error for unknown subcommand \"show\", got nil")
87+
} else if !strings.Contains(err.Error(), `unknown command "show" for "flashduty alert"`) {
88+
t.Fatalf("expected error to identify the unknown command, got %q", err.Error())
89+
}
90+
91+
if _, err := execCommand("channel", "show", "1001"); err == nil {
92+
t.Fatal("expected an error for unknown subcommand \"show\", got nil")
93+
} else if !strings.Contains(err.Error(), `unknown command "show" for "flashduty channel"`) {
94+
t.Fatalf("expected error to identify the unknown command, got %q", err.Error())
95+
}
96+
97+
// A near-miss typo of a real verb still gets a suggestion. (Suggestions
98+
// match command names; "gt" is a 1-edit typo of "get".)
99+
_, err := execCommand("alert", "gt", "alert-1")
100+
if err == nil {
101+
t.Fatal("expected an error for unknown subcommand \"gt\", got nil")
102+
}
103+
if !strings.Contains(err.Error(), "Did you mean this?") {
104+
t.Fatalf("expected a suggestion block, got %q", err.Error())
105+
}
106+
if !strings.Contains(err.Error(), "get") {
107+
t.Fatalf("expected \"get\" to be suggested, got %q", err.Error())
108+
}
109+
}

internal/cli/zz_generated_channels.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/cmd/cligen/main.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,16 @@ var optionalPositional = map[string]bool{
842842
"incidentInfo": true,
843843
}
844844

845+
// opAliases maps an operationId to extra cobra aliases its generated command
846+
// should accept, keyed like positionalOverride. Generated verbs are path-derived
847+
// (channelInfo → "info"), which diverges from the get/detail spelling curated
848+
// commands use for the same single-record read (incident get/detail, alert
849+
// get). Aliases let the generated command answer both spellings so a slip
850+
// (`channel get <id>`) succeeds instead of erroring.
851+
var opAliases = map[string][]string{
852+
"channelInfo": {"get", "detail"},
853+
}
854+
845855
// positional describes the positional argument a generated command exposes.
846856
type positional struct {
847857
Wire string // request-body wire key the positional folds into
@@ -1042,6 +1052,13 @@ func emitCmd(fn string, s service, o specOp, mi methodInfo) string {
10421052
fmt.Fprintf(&b, "\t\tUse: %q,\n", use)
10431053
fmt.Fprintf(&b, "\t\tShort: %q,\n", oneLine(o.Summary))
10441054
fmt.Fprintf(&b, "\t\tLong: %s,\n", quoteMultiline(longHelp(o, scalars, complexFields, specByWire)))
1055+
if aliases := opAliases[o.OpID]; len(aliases) > 0 {
1056+
quoted := make([]string, len(aliases))
1057+
for i, a := range aliases {
1058+
quoted[i] = fmt.Sprintf("%q", a)
1059+
}
1060+
fmt.Fprintf(&b, "\t\tAliases: []string{%s},\n", strings.Join(quoted, ", "))
1061+
}
10451062
if hasPos {
10461063
// Required positionals use body-aware validators: the body field can come
10471064
// from a positional, its typed flag, or --data. Scalar positionals still

0 commit comments

Comments
 (0)