Skip to content

Commit cf1bb53

Browse files
committed
fix(cli): alias alert/channel single-record reads to the get/detail spellings
The single-record read verb is inconsistent across resources: incident answers both "get" and "detail", alert answered only "get", and channel answered only the path-derived "info". The analogy-driven slips (`alert detail <id>`, `channel get <id>`, `channel detail <id>`) failed with "unknown command" even though the intended operation is unambiguous. - alert get gains the "detail" alias, matching incident's get/detail pair. - channel info gains "get" and "detail" aliases. The channel command tree is generated by cligen, so the aliases live in a new opAliases map in the generator (keyed by operationId, the same pattern as positionalOverride) and are emitted into the generated command, so they survive regeneration. Genuinely unknown verbs keep the existing loud failure with did-you-mean suggestions. Tests cover alias resolution to the same wire request, --help through an alias, and the unchanged unknown-verb behavior.
1 parent 9b6dfe9 commit cf1bb53

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)