Skip to content

Commit db1fe88

Browse files
authored
Merge pull request #143 from flashcatcloud/chore/unify-cli-lineage
Merge feat/ai-sre into main — unify the two lineages
2 parents 24fd846 + 3d2e3ca commit db1fe88

27 files changed

Lines changed: 1496 additions & 430 deletions

internal/cli/alert.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func newAlertListCmd() *cobra.Command {
120120
cmd.Flags().BoolVar(&muted, "muted", false, "Show ever-muted only")
121121
cmd.Flags().StringVar(&since, "since", "24h", "Start time")
122122
cmd.Flags().StringVar(&until, "until", "now", "End time")
123-
cmd.Flags().IntVar(&limit, "limit", 20, "Max results")
123+
cmd.Flags().IntVar(&limit, "limit", 20, "Max results (max 100)")
124124
cmd.Flags().IntVar(&page, "page", 1, "Page number")
125125
cmd.Flags().StringVar(&fields, "fields", "", "Comma-separated fields to project in json/toon output (e.g. alert_id,title,alert_severity,created_at); ignored in table mode. Use to avoid dumping the full nested record.")
126126

internal/cli/alert_event.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ func newAlertEventListCmd() *cobra.Command {
9090
fieldNames := []string{"event_id", "alert_id", "event_severity", "event_status", "event_time", "title"}
9191
if fields != "" {
9292
fieldNames = parseStringSlice(fields)
93+
} else {
94+
noteDefaultProjection(cmd.ErrOrStderr(), fieldNames)
9395
}
9496
proj, err := projectFields(result.Items, fieldNames)
9597
if err != nil {
@@ -113,7 +115,7 @@ func newAlertEventListCmd() *cobra.Command {
113115
cmd.Flags().StringVar(&integrationType, "integration-type", "", "Comma-separated integration types (plugin keys, e.g. AliCloud,Prometheus) — not integration IDs; use --integration for that")
114116
cmd.Flags().StringVar(&since, "since", "1h", "Start time")
115117
cmd.Flags().StringVar(&until, "until", "now", "End time")
116-
cmd.Flags().IntVar(&limit, "limit", 20, "Max results")
118+
cmd.Flags().IntVar(&limit, "limit", 20, "Max results (max 100)")
117119
cmd.Flags().IntVar(&page, "page", 1, "Page number")
118120
cmd.Flags().StringVar(&fields, "fields", "", "Comma-separated fields to project in json/toon output (e.g. event_id,alert_id,event_severity,event_status,event_time,title); ignored in table mode. Defaults to these compact event fields. Long strings are truncated as needed to keep structured output below 16 KiB.")
119121

internal/cli/command_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,28 @@ func execCommand(args ...string) (string, error) {
8787
return buf.String(), err
8888
}
8989

90+
// execCommandSplit is execCommand with stdout and stderr captured separately,
91+
// for tests that assert machine-readable stdout stays pure while advisory
92+
// notices (e.g. the default-projection note) land on stderr.
93+
func execCommandSplit(args ...string) (stdout, stderr string, err error) {
94+
resetCommandFlags(rootCmd)
95+
96+
outBuf := new(bytes.Buffer)
97+
errBuf := new(bytes.Buffer)
98+
rootCmd.SetOut(outBuf)
99+
rootCmd.SetErr(errBuf)
100+
rootCmd.SetArgs(args)
101+
102+
err = rootCmd.Execute()
103+
104+
rootCmd.SetArgs(nil)
105+
rootCmd.SetOut(nil)
106+
rootCmd.SetErr(nil)
107+
resetCommandFlags(rootCmd)
108+
109+
return outBuf.String(), errBuf.String(), err
110+
}
111+
90112
func resetCommandFlags(cmd *cobra.Command) {
91113
if cmd == nil {
92114
return

internal/cli/fieldproject.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"fmt"
5+
"io"
56
"reflect"
67
"sort"
78
"strings"
@@ -69,6 +70,16 @@ func projectFields(items any, fields []string) ([]map[string]any, error) {
6970
return out, nil
7071
}
7172

73+
// noteDefaultProjection announces on stderr that structured rows were reduced
74+
// to the command's compact default projection. Without it, a reader piping
75+
// stdout to jq sees an unselected key (labels, description, …) as null on
76+
// every row and can conclude the server never returns it, when it is one
77+
// --fields away. stderr keeps stdout byte-identical for jq/toon pipelines.
78+
func noteDefaultProjection(w io.Writer, fields []string) {
79+
_, _ = fmt.Fprintf(w, "note: rows projected to default compact fields (%s); other response fields are available via --fields\n",
80+
strings.Join(fields, ","))
81+
}
82+
7283
// boundProjectedOutput keeps the new agent-oriented projections below their
7384
// command budget without changing the selected keys. List rows (many small
7485
// records) are shortened fairly when they overflow the budget, with

internal/cli/fieldproject_test.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -213,29 +213,37 @@ func TestIncidentListStructuredDefaultUsesCompactProjection(t *testing.T) {
213213
stub := newGFStub(t)
214214
stub.data = map[string]any{"items": []any{incidentRow()}, "total": 1}
215215

216-
out, err := execCommand("incident", "list", "--output-format", "json")
216+
out, stderrText, err := execCommandSplit("incident", "list", "--output-format", "json")
217217
if err != nil {
218-
t.Fatalf("execCommand: %v", err)
218+
t.Fatalf("execCommandSplit: %v", err)
219219
}
220220

221221
assertProjectedJSONFields(t, out, []string{"incident_id", "title", "incident_severity", "progress", "start_time", "channel_id"})
222+
if !strings.Contains(stderrText, "note: rows projected to default compact fields") {
223+
t.Errorf("default projection should announce itself on stderr, got:\n%s", stderrText)
224+
}
222225
})
223226

224227
t.Run("toon default", func(t *testing.T) {
225228
saveAndResetGlobals(t)
226229
stub := newGFStub(t)
227230
stub.data = map[string]any{"items": []any{incidentRow()}, "total": 1}
228231

229-
out, err := execCommand("incident", "list", "--output-format", "toon")
232+
out, stderrText, err := execCommandSplit("incident", "list", "--output-format", "toon")
230233
if err != nil {
231-
t.Fatalf("execCommand: %v", err)
234+
t.Fatalf("execCommandSplit: %v", err)
232235
}
233236

237+
// Positive keys must come from stdout alone: the stderr note embeds the
238+
// same field names, so a merged capture would satisfy this vacuously.
234239
for _, key := range []string{"incident_id", "title", "incident_severity", "progress", "start_time", "channel_id"} {
235240
if !strings.Contains(out, key) {
236241
t.Errorf("default toon output missing compact key %q, got:\n%s", key, out)
237242
}
238243
}
244+
if !strings.Contains(stderrText, "note: rows projected to default compact fields") {
245+
t.Errorf("default projection should announce itself on stderr, got:\n%s", stderrText)
246+
}
239247
for _, key := range []string{"responders", "labels", "description"} {
240248
if strings.Contains(out, key) {
241249
t.Errorf("default toon output should not contain full-record key %q, got:\n%s", key, out)
@@ -489,13 +497,16 @@ func TestIncidentSimilarStructuredProjection(t *testing.T) {
489497
}
490498
stub.data = map[string]any{"items": items, "total": len(items)}
491499

492-
out, err := execCommand("incident", "similar", "inc-1", "--limit", "20", "--output-format", "json")
500+
out, stderrText, err := execCommandSplit("incident", "similar", "inc-1", "--limit", "20", "--output-format", "json")
493501
if err != nil {
494-
t.Fatalf("execCommand: %v", err)
502+
t.Fatalf("execCommandSplit: %v", err)
495503
}
496504
if len(out) >= 16*1024 {
497505
t.Fatalf("compact similar output is %d bytes, want <16 KiB", len(out))
498506
}
507+
if !strings.Contains(stderrText, "note: rows projected to default compact fields") {
508+
t.Errorf("default projection should announce itself on stderr, got:\n%s", stderrText)
509+
}
499510

500511
var rows []map[string]json.RawMessage
501512
if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &rows); err != nil {
@@ -614,13 +625,16 @@ func TestAlertEventListStructuredProjection(t *testing.T) {
614625
}
615626
stub.data = map[string]any{"items": items, "total": len(items)}
616627

617-
out, err := execCommand("alert-event", "list", "--limit", "30", "--output-format", "json")
628+
out, stderrText, err := execCommandSplit("alert-event", "list", "--limit", "30", "--output-format", "json")
618629
if err != nil {
619-
t.Fatalf("execCommand: %v", err)
630+
t.Fatalf("execCommandSplit: %v", err)
620631
}
621632
if len(out) >= 16*1024 {
622633
t.Fatalf("compact alert-event output is %d bytes, want <16 KiB", len(out))
623634
}
635+
if !strings.Contains(stderrText, "note: rows projected to default compact fields") {
636+
t.Errorf("default projection should announce itself on stderr, got:\n%s", stderrText)
637+
}
624638
var rows []map[string]json.RawMessage
625639
if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &rows); err != nil {
626640
t.Fatalf("parse compact alert-event json: %v\n%s", err, out)

internal/cli/incident.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ func newIncidentListCmd() *cobra.Command {
120120
if len(selectedFields) == 0 {
121121
return fmt.Errorf("--fields must name at least one field")
122122
}
123+
} else {
124+
noteDefaultProjection(cmd.ErrOrStderr(), selectedFields)
123125
}
124126
proj, err := projectFields(result.Items, selectedFields)
125127
if err != nil {
@@ -605,6 +607,8 @@ func newIncidentSimilarCmd() *cobra.Command {
605607
fieldNames := []string{"incident_id", "title", "incident_severity", "progress", "start_time", "close_time", "ack_time", "alert_cnt", "root_cause", "score"}
606608
if fields != "" {
607609
fieldNames = parseStringSlice(fields)
610+
} else {
611+
noteDefaultProjection(cmd.ErrOrStderr(), fieldNames)
608612
}
609613
proj, err := projectFields(result.Items, fieldNames)
610614
if err != nil {

internal/cli/insight.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ func newInsightIncidentsCmd() *cobra.Command {
138138

139139
cmd.Flags().StringVar(&since, "since", "7d", "Start time")
140140
cmd.Flags().StringVar(&until, "until", "now", "End time")
141-
cmd.Flags().IntVar(&limit, "limit", 20, "Max results")
141+
cmd.Flags().IntVar(&limit, "limit", 20, "Max results (max 100)")
142142
cmd.Flags().IntVar(&page, "page", 1, "Page number")
143143

144144
return cmd

internal/cli/zz_generated_alerts.go

Lines changed: 21 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)