Skip to content

Commit e64526a

Browse files
authored
Merge pull request #53 from flashcatcloud/feat/ai-sre
Feat/ai sre
2 parents baa80f7 + d4a8e55 commit e64526a

15 files changed

Lines changed: 114 additions & 193 deletions

e2e/edge_case_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,11 @@ func TestJSONOnAckCommand(t *testing.T) {
134134
id := extractIncidentID(t, r.Stdout)
135135
t.Cleanup(func() { runCLI(t, "incident", "close", id) })
136136

137+
// ack is served by the generated twin; --json wraps the OK line as {"message":"..."}.
137138
r = runCLI(t, "incident", "ack", id, "--json")
138139
requireSuccess(t, r)
139140
requireValidJSON(t, r.Stdout)
140-
requireContains(t, r.Stdout, "Acknowledged")
141+
requireContains(t, r.Stdout, "OK: POST /incident/ack")
141142
}
142143

143144
// Test 307: --json on close command

e2e/incident_extended_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,10 @@ func TestIncidentAckSingleID(t *testing.T) {
192192
id := extractIncidentID(t, r.Stdout)
193193
t.Cleanup(func() { runCLI(t, "incident", "close", id) })
194194

195+
// Served by the generated twin (positional id → incident_ids).
195196
r = runCLI(t, "incident", "ack", id)
196197
requireSuccess(t, r)
197-
requireContains(t, r.Stdout, "Acknowledged 1 incident(s).")
198+
requireContains(t, r.Stdout, "OK: POST /incident/ack")
198199
}
199200

200201
// Test 204: close single ID

e2e/incident_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,10 @@ func TestIncidentLifecycle(t *testing.T) {
101101
requireContains(t, r.Stdout, "Triggered")
102102
requireContains(t, r.Stdout, name)
103103

104-
// Step 3: Ack
104+
// Step 3: Ack (served by the generated twin; positional id → incident_ids).
105105
r = runCLI(t, "incident", "ack", id)
106106
requireSuccess(t, r)
107-
requireContains(t, r.Stdout, "Acknowledged 1 incident(s).")
107+
requireContains(t, r.Stdout, "OK: POST /incident/ack")
108108

109109
// Step 4: Get - should be Processing
110110
r = runCLI(t, "incident", "get", id)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/flashcatcloud/flashduty-cli
33
go 1.25.1
44

55
require (
6-
github.com/flashcatcloud/go-flashduty v0.5.4-0.20260602051355-7583ebae5b07
6+
github.com/flashcatcloud/go-flashduty v0.5.4-0.20260616041609-da82c4097dd1
77
github.com/mattn/go-runewidth v0.0.24
88
github.com/spf13/cobra v1.10.2
99
github.com/spf13/pflag v1.0.10

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
22
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
33
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
4-
github.com/flashcatcloud/go-flashduty v0.5.4-0.20260602051355-7583ebae5b07 h1:bi1rOjR2OY+TovBGabtVOTcEQWlgzU9RfEwlJxU+3n8=
5-
github.com/flashcatcloud/go-flashduty v0.5.4-0.20260602051355-7583ebae5b07/go.mod h1:aA0RtZEs0AYOwwdNKdtVeD8YMOdnmVY1zAlVD+9Ovx8=
4+
github.com/flashcatcloud/go-flashduty v0.5.4-0.20260616041609-da82c4097dd1 h1:K/TceO2NHUPAB8Ew7p/7y6gGDjokNpHyd30uxi8FApc=
5+
github.com/flashcatcloud/go-flashduty v0.5.4-0.20260616041609-da82c4097dd1/go.mod h1:aA0RtZEs0AYOwwdNKdtVeD8YMOdnmVY1zAlVD+9Ovx8=
66
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
77
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
88
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=

internal/cli/alert.go

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ func newAlertCmd() *cobra.Command {
2222
cmd.AddCommand(newAlertGetCmd())
2323
cmd.AddCommand(newAlertEventsCmd())
2424
cmd.AddCommand(newAlertTimelineCmd())
25-
cmd.AddCommand(newAlertMergeCmd())
25+
// merge is registered via the generated layer (positional alert-ids fold to
26+
// alert_ids). Flag-name change: --incident (curated) → --incident-id (generated).
2627
return cmd
2728
}
2829

@@ -303,33 +304,3 @@ func resolveAlertFeedOperators(rc *RunContext, items []flashduty.FeedItem) map[i
303304
}
304305
return out
305306
}
306-
307-
func newAlertMergeCmd() *cobra.Command {
308-
var incidentID, comment string
309-
310-
cmd := &cobra.Command{
311-
Use: "merge <alert_id> [<alert_id2> ...]",
312-
Short: "Merge alerts into an incident",
313-
Args: requireArgs("alert_id"),
314-
RunE: func(cmd *cobra.Command, args []string) error {
315-
return runCommand(cmd, args, func(ctx *RunContext) error {
316-
if _, err := ctx.Client.Alerts.WriteMerge(cmdContext(ctx.Cmd), &flashduty.AlertMergeRequest{
317-
AlertIDs: ctx.Args,
318-
IncidentID: incidentID,
319-
Comment: comment,
320-
}); err != nil {
321-
return err
322-
}
323-
324-
ctx.WriteResult(fmt.Sprintf("Merged %d alert(s) into incident %s.", len(ctx.Args), incidentID))
325-
return nil
326-
})
327-
},
328-
}
329-
330-
cmd.Flags().StringVar(&incidentID, "incident", "", "Target incident ID")
331-
cmd.Flags().StringVar(&comment, "comment", "", "Merge comment")
332-
_ = cmd.MarkFlagRequired("incident")
333-
334-
return cmd
335-
}

internal/cli/args.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,24 @@ func requireExactArg(name string) cobra.PositionalArgs {
4141
}
4242
}
4343

44+
// optionalArg returns a positional argument validator that accepts zero or one
45+
// argument named name. It backs generated commands whose positional folds into
46+
// an OPTIONAL body field because the operation also accepts an alternative
47+
// lookup key via a flag (e.g. `incident info` takes either the <incident-id>
48+
// positional or --num). Extra arguments are rejected rather than silently
49+
// dropped:
50+
//
51+
// - zero or one arg: ok
52+
// - >1 args: "expects at most one <name>. Usage: ..."
53+
func optionalArg(name string) cobra.PositionalArgs {
54+
return func(cmd *cobra.Command, args []string) error {
55+
if len(args) > 1 {
56+
return fmt.Errorf("expects at most one %s. Usage: %s", name, cmd.UseLine())
57+
}
58+
return nil
59+
}
60+
}
61+
4462
// requireExactlyOneFlag validates that exactly one of the named flags is set.
4563
func requireExactlyOneFlag(cmd *cobra.Command, flagNames ...string) error {
4664
set := 0

internal/cli/channel.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,21 @@ type channelRow struct {
3939

4040
func newChannelListCmd() *cobra.Command {
4141
var name string
42+
var teamIDs []int64
4243

4344
cmd := &cobra.Command{
4445
Use: "list",
4546
Short: "List channels",
46-
Long: curatedLong("List channels in the account, optionally filtered by name.", "Channels", "ChannelList"),
47+
Long: curatedLong("List channels in the account, optionally filtered by name or owning team.", "Channels", "ChannelList"),
4748
RunE: func(cmd *cobra.Command, args []string) error {
4849
return runCommand(cmd, args, func(ctx *RunContext) error {
4950
// Legacy parity: the hand-written SDK called /channel/list with an
5051
// empty body and applied the --name filter client-side as a
5152
// case-insensitive substring match. go-flashduty's ChannelName field
5253
// is an exact-match server filter, so we keep the client-side filter
53-
// to preserve behavior.
54-
result, _, err := ctx.Client.Channels.ChannelList(cmdContext(ctx.Cmd), &flashduty.ListChannelsRequest{})
54+
// to preserve behavior. --team-ids, by contrast, is a server-side
55+
// filter on the channel's owning team (empty = all teams, unchanged).
56+
result, _, err := ctx.Client.Channels.ChannelList(cmdContext(ctx.Cmd), &flashduty.ListChannelsRequest{TeamIDs: teamIDs})
5557
if err != nil {
5658
return err
5759
}
@@ -87,6 +89,7 @@ func newChannelListCmd() *cobra.Command {
8789
}
8890

8991
cmd.Flags().StringVar(&name, "name", "", "Search by name")
92+
cmd.Flags().Int64SliceVar(&teamIDs, "team-ids", nil, "Filter by owning team ID(s), server-side (repeatable or comma-separated)")
9093

9194
return cmd
9295
}

internal/cli/command_test.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ func TestCommandIncidentUnack(t *testing.T) {
430430
saveAndResetGlobals(t)
431431
stub := newGFStub(t)
432432

433+
// unack is served by the generated twin (positional ids → incident_ids).
433434
out, err := execCommand("incident", "unack", "inc-1", "inc-2")
434435
if err != nil {
435436
t.Fatalf("[incident-unack] unexpected error: %v", err)
@@ -440,7 +441,7 @@ func TestCommandIncidentUnack(t *testing.T) {
440441
if got, want := strings.Join(stub.bodyStrings("incident_ids"), ","), "inc-1,inc-2"; got != want {
441442
t.Fatalf("[incident-unack] expected ids %q, got %q", want, got)
442443
}
443-
if !strings.Contains(out, "Unacknowledged 2 incident(s).") {
444+
if !strings.Contains(out, "OK: POST /incident/unack") {
444445
t.Fatalf("[incident-unack] unexpected output:\n%s", out)
445446
}
446447
}
@@ -449,6 +450,7 @@ func TestCommandIncidentWake(t *testing.T) {
449450
saveAndResetGlobals(t)
450451
stub := newGFStub(t)
451452

453+
// wake is served by the generated twin (positional id → incident_ids).
452454
out, err := execCommand("incident", "wake", "inc-1")
453455
if err != nil {
454456
t.Fatalf("[incident-wake] unexpected error: %v", err)
@@ -459,7 +461,7 @@ func TestCommandIncidentWake(t *testing.T) {
459461
if got, want := strings.Join(stub.bodyStrings("incident_ids"), ","), "inc-1"; got != want {
460462
t.Fatalf("[incident-wake] expected ids %q, got %q", want, got)
461463
}
462-
if !strings.Contains(out, "Restored notifications for 1 incident(s).") {
464+
if !strings.Contains(out, "OK: POST /incident/wake") {
463465
t.Fatalf("[incident-wake] unexpected output:\n%s", out)
464466
}
465467
}
@@ -500,13 +502,15 @@ func TestCommandIncidentCommentAllows1024UnicodeRunes(t *testing.T) {
500502
}
501503
}
502504

505+
// TestCommandIncidentLifecycleRejectsMoreThan100IDs covers the curated
506+
// commands that still enforce the 100-id batch cap client-side. unack and wake
507+
// were dropped in favor of their generated twins, which carry no client-side
508+
// cap (the backend enforces the limit), so they are intentionally absent here.
503509
func TestCommandIncidentLifecycleRejectsMoreThan100IDs(t *testing.T) {
504510
commands := []struct {
505511
name string
506512
args []string
507513
}{
508-
{name: "unack", args: []string{"incident", "unack"}},
509-
{name: "wake", args: []string{"incident", "wake"}},
510514
{name: "comment", args: []string{"incident", "comment", "--comment", "too many"}},
511515
{name: "remove", args: []string{"incident", "remove"}},
512516
}
@@ -612,6 +616,7 @@ func TestCommandIncidentDisableMerge(t *testing.T) {
612616
saveAndResetGlobals(t)
613617
stub := newGFStub(t)
614618

619+
// disable-merge is served by the generated twin (positional ids → incident_ids).
615620
out, err := execCommand("incident", "disable-merge", "inc-1", "inc-2")
616621
if err != nil {
617622
t.Fatalf("[incident-disable-merge] unexpected error: %v", err)
@@ -622,7 +627,7 @@ func TestCommandIncidentDisableMerge(t *testing.T) {
622627
if got, want := strings.Join(stub.bodyStrings("incident_ids"), ","), "inc-1,inc-2"; got != want {
623628
t.Fatalf("[incident-disable-merge] expected ids %q, got %q", want, got)
624629
}
625-
if !strings.Contains(out, "Disabled auto-merge for 2 incident(s).") {
630+
if !strings.Contains(out, "OK: POST /incident/disable-merge") {
626631
t.Fatalf("[incident-disable-merge] unexpected output:\n%s", out)
627632
}
628633
}

internal/cli/gen_positional_test.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import (
1111
// the variadic marker; an *_id scalar op renders the single id; the override and
1212
// int cases render their pinned field.
1313
func TestGenPositionalUseLine(t *testing.T) {
14-
// (a) and (b) read the generated constructors directly (the curated commands
15-
// own the live `incident ack`/`incident info` path-names, so the generated
16-
// twins are dropped at registration but still constructible for assertion).
14+
// (a) and (b) call the generated constructors directly so the Use-line
15+
// assertions stay independent of registration order. `incident ack` now
16+
// surfaces the generated twin (curated shadow dropped); `incident info` was
17+
// always generated-only (the curated leaf is named `detail`).
1718
ack := genIncidentsAckCmd()
1819
if got := ack.Use; got != "ack <incident-id> [<id2>...]" {
1920
t.Errorf("ack twin Use = %q, want %q", got, "ack <incident-id> [<id2>...]")
@@ -28,19 +29,26 @@ func TestGenPositionalUseLine(t *testing.T) {
2829
t.Errorf("ack twin Args rejected one arg: %v", err)
2930
}
3031

32+
// `incident info` pins incident_id as an OPTIONAL positional: the backend
33+
// relaxed incident_id (a lookup may instead pass the 6-char num via --num), so
34+
// the positional is 0-or-1. `info <id>` stays for back-compat; `info` alone
35+
// (with --num) is valid; `info id1 id2` is still rejected.
3136
info := genIncidentsInfoCmd()
32-
if got := info.Use; got != "info <incident-id>" {
33-
t.Errorf("info twin Use = %q, want %q", got, "info <incident-id>")
37+
if got := info.Use; got != "info [<incident-id>]" {
38+
t.Errorf("info twin Use = %q, want %q", got, "info [<incident-id>]")
3439
}
3540
if info.Args == nil {
3641
t.Errorf("info twin has no Args validator")
3742
}
38-
if err := info.Args(info, nil); err == nil {
39-
t.Errorf("info twin Args accepted zero args (want exactly one)")
43+
if err := info.Args(info, nil); err != nil {
44+
t.Errorf("info twin Args rejected zero args (want 0-or-1; --num path): %v", err)
4045
}
4146
if err := info.Args(info, []string{"id1"}); err != nil {
4247
t.Errorf("info twin Args rejected one arg: %v", err)
4348
}
49+
if err := info.Args(info, []string{"id1", "id2"}); err == nil {
50+
t.Errorf("info twin Args accepted two args (want at most one)")
51+
}
4452

4553
// Override cases: merge pins target_incident_id (NOT source_incident_ids);
4654
// war-room detail pins chat_id.

0 commit comments

Comments
 (0)