diff --git a/e2e/edge_case_test.go b/e2e/edge_case_test.go index ae56d14..ad9d4cf 100644 --- a/e2e/edge_case_test.go +++ b/e2e/edge_case_test.go @@ -134,10 +134,11 @@ func TestJSONOnAckCommand(t *testing.T) { id := extractIncidentID(t, r.Stdout) t.Cleanup(func() { runCLI(t, "incident", "close", id) }) + // ack is served by the generated twin; --json wraps the OK line as {"message":"..."}. r = runCLI(t, "incident", "ack", id, "--json") requireSuccess(t, r) requireValidJSON(t, r.Stdout) - requireContains(t, r.Stdout, "Acknowledged") + requireContains(t, r.Stdout, "OK: POST /incident/ack") } // Test 307: --json on close command diff --git a/e2e/incident_extended_test.go b/e2e/incident_extended_test.go index 20aa507..96e6660 100644 --- a/e2e/incident_extended_test.go +++ b/e2e/incident_extended_test.go @@ -192,9 +192,10 @@ func TestIncidentAckSingleID(t *testing.T) { id := extractIncidentID(t, r.Stdout) t.Cleanup(func() { runCLI(t, "incident", "close", id) }) + // Served by the generated twin (positional id → incident_ids). r = runCLI(t, "incident", "ack", id) requireSuccess(t, r) - requireContains(t, r.Stdout, "Acknowledged 1 incident(s).") + requireContains(t, r.Stdout, "OK: POST /incident/ack") } // Test 204: close single ID diff --git a/e2e/incident_test.go b/e2e/incident_test.go index 9a9ff1a..2019b47 100644 --- a/e2e/incident_test.go +++ b/e2e/incident_test.go @@ -101,10 +101,10 @@ func TestIncidentLifecycle(t *testing.T) { requireContains(t, r.Stdout, "Triggered") requireContains(t, r.Stdout, name) - // Step 3: Ack + // Step 3: Ack (served by the generated twin; positional id → incident_ids). r = runCLI(t, "incident", "ack", id) requireSuccess(t, r) - requireContains(t, r.Stdout, "Acknowledged 1 incident(s).") + requireContains(t, r.Stdout, "OK: POST /incident/ack") // Step 4: Get - should be Processing r = runCLI(t, "incident", "get", id) diff --git a/internal/cli/alert.go b/internal/cli/alert.go index a08254e..6f21258 100644 --- a/internal/cli/alert.go +++ b/internal/cli/alert.go @@ -22,7 +22,8 @@ func newAlertCmd() *cobra.Command { cmd.AddCommand(newAlertGetCmd()) cmd.AddCommand(newAlertEventsCmd()) cmd.AddCommand(newAlertTimelineCmd()) - cmd.AddCommand(newAlertMergeCmd()) + // merge is registered via the generated layer (positional alert-ids fold to + // alert_ids). Flag-name change: --incident (curated) → --incident-id (generated). return cmd } @@ -303,33 +304,3 @@ func resolveAlertFeedOperators(rc *RunContext, items []flashduty.FeedItem) map[i } return out } - -func newAlertMergeCmd() *cobra.Command { - var incidentID, comment string - - cmd := &cobra.Command{ - Use: "merge [ ...]", - Short: "Merge alerts into an incident", - Args: requireArgs("alert_id"), - RunE: func(cmd *cobra.Command, args []string) error { - return runCommand(cmd, args, func(ctx *RunContext) error { - if _, err := ctx.Client.Alerts.WriteMerge(cmdContext(ctx.Cmd), &flashduty.AlertMergeRequest{ - AlertIDs: ctx.Args, - IncidentID: incidentID, - Comment: comment, - }); err != nil { - return err - } - - ctx.WriteResult(fmt.Sprintf("Merged %d alert(s) into incident %s.", len(ctx.Args), incidentID)) - return nil - }) - }, - } - - cmd.Flags().StringVar(&incidentID, "incident", "", "Target incident ID") - cmd.Flags().StringVar(&comment, "comment", "", "Merge comment") - _ = cmd.MarkFlagRequired("incident") - - return cmd -} diff --git a/internal/cli/command_test.go b/internal/cli/command_test.go index 6915794..0def636 100644 --- a/internal/cli/command_test.go +++ b/internal/cli/command_test.go @@ -430,6 +430,7 @@ func TestCommandIncidentUnack(t *testing.T) { saveAndResetGlobals(t) stub := newGFStub(t) + // unack is served by the generated twin (positional ids → incident_ids). out, err := execCommand("incident", "unack", "inc-1", "inc-2") if err != nil { t.Fatalf("[incident-unack] unexpected error: %v", err) @@ -440,7 +441,7 @@ func TestCommandIncidentUnack(t *testing.T) { if got, want := strings.Join(stub.bodyStrings("incident_ids"), ","), "inc-1,inc-2"; got != want { t.Fatalf("[incident-unack] expected ids %q, got %q", want, got) } - if !strings.Contains(out, "Unacknowledged 2 incident(s).") { + if !strings.Contains(out, "OK: POST /incident/unack") { t.Fatalf("[incident-unack] unexpected output:\n%s", out) } } @@ -449,6 +450,7 @@ func TestCommandIncidentWake(t *testing.T) { saveAndResetGlobals(t) stub := newGFStub(t) + // wake is served by the generated twin (positional id → incident_ids). out, err := execCommand("incident", "wake", "inc-1") if err != nil { t.Fatalf("[incident-wake] unexpected error: %v", err) @@ -459,7 +461,7 @@ func TestCommandIncidentWake(t *testing.T) { if got, want := strings.Join(stub.bodyStrings("incident_ids"), ","), "inc-1"; got != want { t.Fatalf("[incident-wake] expected ids %q, got %q", want, got) } - if !strings.Contains(out, "Restored notifications for 1 incident(s).") { + if !strings.Contains(out, "OK: POST /incident/wake") { t.Fatalf("[incident-wake] unexpected output:\n%s", out) } } @@ -500,13 +502,15 @@ func TestCommandIncidentCommentAllows1024UnicodeRunes(t *testing.T) { } } +// TestCommandIncidentLifecycleRejectsMoreThan100IDs covers the curated +// commands that still enforce the 100-id batch cap client-side. unack and wake +// were dropped in favor of their generated twins, which carry no client-side +// cap (the backend enforces the limit), so they are intentionally absent here. func TestCommandIncidentLifecycleRejectsMoreThan100IDs(t *testing.T) { commands := []struct { name string args []string }{ - {name: "unack", args: []string{"incident", "unack"}}, - {name: "wake", args: []string{"incident", "wake"}}, {name: "comment", args: []string{"incident", "comment", "--comment", "too many"}}, {name: "remove", args: []string{"incident", "remove"}}, } @@ -612,6 +616,7 @@ func TestCommandIncidentDisableMerge(t *testing.T) { saveAndResetGlobals(t) stub := newGFStub(t) + // disable-merge is served by the generated twin (positional ids → incident_ids). out, err := execCommand("incident", "disable-merge", "inc-1", "inc-2") if err != nil { t.Fatalf("[incident-disable-merge] unexpected error: %v", err) @@ -622,7 +627,7 @@ func TestCommandIncidentDisableMerge(t *testing.T) { if got, want := strings.Join(stub.bodyStrings("incident_ids"), ","), "inc-1,inc-2"; got != want { t.Fatalf("[incident-disable-merge] expected ids %q, got %q", want, got) } - if !strings.Contains(out, "Disabled auto-merge for 2 incident(s).") { + if !strings.Contains(out, "OK: POST /incident/disable-merge") { t.Fatalf("[incident-disable-merge] unexpected output:\n%s", out) } } diff --git a/internal/cli/gen_positional_test.go b/internal/cli/gen_positional_test.go index d177629..434da0c 100644 --- a/internal/cli/gen_positional_test.go +++ b/internal/cli/gen_positional_test.go @@ -11,9 +11,10 @@ import ( // the variadic marker; an *_id scalar op renders the single id; the override and // int cases render their pinned field. func TestGenPositionalUseLine(t *testing.T) { - // (a) and (b) read the generated constructors directly (the curated commands - // own the live `incident ack`/`incident info` path-names, so the generated - // twins are dropped at registration but still constructible for assertion). + // (a) and (b) call the generated constructors directly so the Use-line + // assertions stay independent of registration order. `incident ack` now + // surfaces the generated twin (curated shadow dropped); `incident info` was + // always generated-only (the curated leaf is named `detail`). ack := genIncidentsAckCmd() if got := ack.Use; got != "ack [...]" { t.Errorf("ack twin Use = %q, want %q", got, "ack [...]") diff --git a/internal/cli/incident.go b/internal/cli/incident.go index 90e4b3a..61515bb 100644 --- a/internal/cli/incident.go +++ b/internal/cli/incident.go @@ -28,20 +28,18 @@ func newIncidentCmd() *cobra.Command { cmd.AddCommand(newIncidentGetCmd()) cmd.AddCommand(newIncidentCreateCmd()) cmd.AddCommand(newIncidentUpdateCmd()) - cmd.AddCommand(newIncidentAckCmd()) - cmd.AddCommand(newIncidentUnackCmd()) + // ack, unack, wake, reopen, and disable-merge are registered via the + // generated layer (positional ids fold to incident_ids; flags are a superset + // of the dropped curated shadows). cmd.AddCommand(newIncidentCloseCmd()) - cmd.AddCommand(newIncidentWakeCmd()) cmd.AddCommand(newIncidentTimelineCmd()) cmd.AddCommand(newIncidentAlertsCmd()) cmd.AddCommand(newIncidentSimilarCmd()) cmd.AddCommand(newIncidentMergeCmd()) cmd.AddCommand(newIncidentSnoozeCmd()) - cmd.AddCommand(newIncidentReopenCmd()) cmd.AddCommand(newIncidentReassignCmd()) cmd.AddCommand(newIncidentAddResponderCmd()) cmd.AddCommand(newIncidentCommentCmd()) - cmd.AddCommand(newIncidentDisableMergeCmd()) cmd.AddCommand(newIncidentRemoveCmd()) cmd.AddCommand(newIncidentWarRoomCmd()) cmd.AddCommand(newIncidentFeedCmd()) @@ -441,53 +439,6 @@ func newIncidentUpdateCmd() *cobra.Command { return cmd } -func newIncidentAckCmd() *cobra.Command { - return &cobra.Command{ - Use: "ack [ ...]", - Short: "Acknowledge incidents", - Args: requireArgs("incident_id"), - RunE: func(cmd *cobra.Command, args []string) error { - return runCommand(cmd, args, func(ctx *RunContext) error { - if _, err := ctx.Client.Incidents.Ack(cmdContext(ctx.Cmd), &flashduty.AckIncidentRequest{ - IncidentIDs: ctx.Args, - }); err != nil { - return err - } - ctx.WriteResult(fmt.Sprintf("Acknowledged %d incident(s).", len(ctx.Args))) - return nil - }) - }, - } -} - -func newIncidentUnackCmd() *cobra.Command { - return &cobra.Command{ - Use: "unack [ ...]", - Short: "Cancel incident acknowledgement", - Long: `Cancel acknowledgement for one or more incidents. - -Use this when an incident was acknowledged by mistake and should return to the -unacknowledged state. The command accepts up to 100 incident IDs.`, - Example: ` flashduty incident unack inc_123 - flashduty incident unack inc_123 inc_456`, - Args: requireArgs("incident_id"), - RunE: func(cmd *cobra.Command, args []string) error { - if err := validateIncidentIDBatch(args); err != nil { - return err - } - return runCommand(cmd, args, func(ctx *RunContext) error { - if _, err := ctx.Client.Incidents.Unack(cmdContext(ctx.Cmd), &flashduty.UnackIncidentRequest{ - IncidentIDs: ctx.Args, - }); err != nil { - return err - } - ctx.WriteResult(fmt.Sprintf("Unacknowledged %d incident(s).", len(ctx.Args))) - return nil - }) - }, - } -} - func newIncidentCloseCmd() *cobra.Command { return &cobra.Command{ Use: "close [ ...]", @@ -507,34 +458,6 @@ func newIncidentCloseCmd() *cobra.Command { } } -func newIncidentWakeCmd() *cobra.Command { - return &cobra.Command{ - Use: "wake [ ...]", - Short: "Restore notifications for snoozed incidents", - Long: `Wake one or more snoozed incidents. - -This cancels snooze and restores normal incident notifications. The command -accepts up to 100 incident IDs.`, - Example: ` flashduty incident wake inc_123 - flashduty incident wake inc_123 inc_456`, - Args: requireArgs("incident_id"), - RunE: func(cmd *cobra.Command, args []string) error { - if err := validateIncidentIDBatch(args); err != nil { - return err - } - return runCommand(cmd, args, func(ctx *RunContext) error { - if _, err := ctx.Client.Incidents.Wake(cmdContext(ctx.Cmd), &flashduty.WakeIncidentRequest{ - IncidentIDs: ctx.Args, - }); err != nil { - return err - } - ctx.WriteResult(fmt.Sprintf("Restored notifications for %d incident(s).", len(ctx.Args))) - return nil - }) - }, - } -} - func newIncidentTimelineCmd() *cobra.Command { return &cobra.Command{ Use: "timeline ", @@ -784,25 +707,6 @@ func newIncidentSnoozeCmd() *cobra.Command { return cmd } -func newIncidentReopenCmd() *cobra.Command { - return &cobra.Command{ - Use: "reopen [ ...]", - Short: "Reopen closed incidents", - Args: requireArgs("incident_id"), - RunE: func(cmd *cobra.Command, args []string) error { - return runCommand(cmd, args, func(ctx *RunContext) error { - if _, err := ctx.Client.Incidents.Reopen(cmdContext(ctx.Cmd), &flashduty.ReopenIncidentRequest{ - IncidentIDs: ctx.Args, - }); err != nil { - return err - } - ctx.WriteResult(fmt.Sprintf("Reopened %d incident(s).", len(ctx.Args))) - return nil - }) - }, - } -} - func newIncidentReassignCmd() *cobra.Command { var person string @@ -952,31 +856,6 @@ webhook reply behavior.`, return cmd } -func newIncidentDisableMergeCmd() *cobra.Command { - return &cobra.Command{ - Use: "disable-merge [ ...]", - Short: "Disable automatic merging for incidents", - Long: `Disable automatic alert merging for one or more incidents. - -Use this when an incident should stay isolated and must not absorb additional -matching alerts automatically. The command accepts up to 100 incident IDs.`, - Example: ` flashduty incident disable-merge inc_123 - flashduty incident disable-merge inc_123 inc_456`, - Args: requireArgs("incident_id"), - RunE: func(cmd *cobra.Command, args []string) error { - return runCommand(cmd, args, func(ctx *RunContext) error { - if _, err := ctx.Client.Incidents.DisableMerge(cmdContext(ctx.Cmd), &flashduty.DisableIncidentMergeRequest{ - IncidentIDs: ctx.Args, - }); err != nil { - return err - } - ctx.WriteResult(fmt.Sprintf("Disabled auto-merge for %d incident(s).", len(ctx.Args))) - return nil - }) - }, - } -} - func newIncidentRemoveCmd() *cobra.Command { var force bool