diff --git a/internal/cli/alert.go b/internal/cli/alert.go index 8317f07..4660d3b 100644 --- a/internal/cli/alert.go +++ b/internal/cli/alert.go @@ -96,7 +96,7 @@ func newAlertListCmd() *cobra.Command { if err != nil { return err } - return ctx.PrintList(proj, nil, len(result.Items), page, limit, int(result.Total)) + return ctx.PrintCappedList(proj, nil, len(result.Items), page, limit, int(result.Total), result.HasNextPage) } cols := []output.Column{ @@ -109,7 +109,7 @@ func newAlertListCmd() *cobra.Command { {Header: "STARTED", Field: func(v any) string { return output.FormatTime(v.(flashduty.AlertItem).StartTime) }}, } - return ctx.PrintList(result.Items, cols, len(result.Items), page, limit, int(result.Total)) + return ctx.PrintCappedList(result.Items, cols, len(result.Items), page, limit, int(result.Total), result.HasNextPage) }) }, } @@ -236,7 +236,7 @@ func newAlertEventsCmd() *cobra.Command { defaults := listDefaultsFor("Alerts", "ReadEventList") win := requestWindow(ctx.Cmd, nil, defaults) return ctx.PrintPage(result.Items, cols, listPage{ - Count: len(result.Items), Page: win.page, Base: win.base, Limit: win.limit, Total: int(result.Total), More: moreOf(result.HasNextPage), + Count: len(result.Items), Page: win.page, Base: win.base, Limit: win.limit, Total: int(result.Total), More: moreOf(result.HasNextPage), TotalCapped: true, }) }) }, diff --git a/internal/cli/alert_event.go b/internal/cli/alert_event.go index 0721fa5..dc4325c 100644 --- a/internal/cli/alert_event.go +++ b/internal/cli/alert_event.go @@ -104,10 +104,10 @@ func newAlertEventListCmd() *cobra.Command { if len(proj) < len(result.Items) { effectiveLimit = len(proj) } - return ctx.PrintList(proj, nil, len(proj), page, effectiveLimit, int(result.Total)) + return ctx.PrintCappedList(proj, nil, len(proj), page, effectiveLimit, int(result.Total), result.HasNextPage) } - return ctx.PrintList(result.Items, cols, len(result.Items), page, limit, int(result.Total)) + return ctx.PrintCappedList(result.Items, cols, len(result.Items), page, limit, int(result.Total), result.HasNextPage) }) }, } diff --git a/internal/cli/command.go b/internal/cli/command.go index eea95b0..a18c417 100644 --- a/internal/cli/command.go +++ b/internal/cli/command.go @@ -3,6 +3,7 @@ package cli import ( "fmt" "io" + "strconv" "github.com/flashcatcloud/go-flashduty" "github.com/spf13/cobra" @@ -98,12 +99,24 @@ type listPage struct { Limit int // effective page size: requested, else the schema default; 0 when unknown Total int // total matching rows; negative when the API returned none More moreState // has_next_page / next cursor, when the API returned one + // TotalCapped marks an endpoint that stops counting at serverCountCap; its + // callers must also set More, the only "rows remain" signal past the cap. + TotalCapped bool } // totalKnown reports whether Total is a usable server total. A total below the // shown count is an absent field, not a real total. func (p listPage) totalKnown() bool { return p.Total >= 0 && p.Total >= p.Count } +// serverCountCap is where the incident, alert and alert-event list endpoints +// stop counting: past it they report this value as the total while rows keep +// coming (the alert and alert-event API reference says "capped at 1000"). +const serverCountCap = 1000 + +// totalIsFloor reports whether Total is a capped count, which says only that at +// least that many rows match. +func (p listPage) totalIsFloor() bool { return p.TotalCapped && p.Total >= serverCountCap } + // offset returns how many rows precede this page, when that is knowable: always // for the first page, and for later pages only with a known page size and base. func (p listPage) offset() (int, bool) { @@ -143,6 +156,8 @@ func results(n int) string { // footer and the structured-mode stderr note ("" when no warning applies): // // a. total known: "showing N of T", plus a next-page hint when rows remain; +// a capped total reads "at least T", and has_next_page alone decides +// whether rows remain; // b. else has_next_page/cursor known: true warns, false is complete; // c. else a full page (N == limit) cannot tell: warn that more may exist; // d. else only the plain count. @@ -179,13 +194,18 @@ func (p listPage) statement(cmd *cobra.Command) (footer, note string) { } switch { case p.totalKnown(): - footer = fmt.Sprintf("Showing %s%s.", results(p.Count), paren(fmt.Sprintf("total %d", p.Total))) - if off, ok := p.offset(); ok && off+p.Count < p.Total { - body := fmt.Sprintf("showing %d of %d total results%s", p.Count, p.Total, paren("")) - if hint != "" { - body += ": " + hint - } - note = "note: " + body + total := strconv.Itoa(p.Total) + if p.totalIsFloor() { + total = "at least " + total + } + footer = fmt.Sprintf("Showing %s%s.", results(p.Count), paren("total "+total)) + off, offKnown := p.offset() + remain := offKnown && off+p.Count < p.Total + if p.totalIsFloor() { + remain = p.More == moreYes + } + if remain { + _, note = warn(fmt.Sprintf("%d of %s total results%s", p.Count, total, paren("")), "") } return footer, note case p.More == moreYes: @@ -257,6 +277,14 @@ func (ctx *RunContext) PrintList(items any, cols []output.Column, count, page, l return ctx.PrintPage(items, cols, listPage{Count: count, Page: page, Base: pageOne, Limit: limit, Total: total}) } +// PrintCappedList is PrintList for an endpoint whose total stops at +// serverCountCap; hasNext is the response's has_next_page. +func (ctx *RunContext) PrintCappedList(items any, cols []output.Column, count, page, limit, total int, hasNext bool) error { + return ctx.PrintPage(items, cols, listPage{ + Count: count, Page: page, Base: pageOne, Limit: limit, Total: total, More: moreOf(hasNext), TotalCapped: true, + }) +} + // WriteResult prints a success message as plain text or JSON. func (ctx *RunContext) WriteResult(message string) { writeResult(ctx.Writer, message) diff --git a/internal/cli/incident.go b/internal/cli/incident.go index 04aa10f..547dd63 100644 --- a/internal/cli/incident.go +++ b/internal/cli/incident.go @@ -173,10 +173,10 @@ func newIncidentListCmd() *cobra.Command { if len(proj) < len(result.Items) { effectiveLimit = len(proj) } - return ctx.PrintList(proj, nil, len(proj), page, effectiveLimit, int(result.Total)) + return ctx.PrintCappedList(proj, nil, len(proj), page, effectiveLimit, int(result.Total), result.HasNextPage) } - return ctx.PrintList(result.Items, incidentColumns(), len(result.Items), page, limit, int(result.Total)) + return ctx.PrintCappedList(result.Items, incidentColumns(), len(result.Items), page, limit, int(result.Total), result.HasNextPage) }) }, } diff --git a/internal/cli/list_completeness_test.go b/internal/cli/list_completeness_test.go index 2291998..9157279 100644 --- a/internal/cli/list_completeness_test.go +++ b/internal/cli/list_completeness_test.go @@ -99,6 +99,45 @@ func TestListPageStatement(t *testing.T) { wantFooter: "Showing 5 results (page 1, total 45).", wantNote: "note: showing 5 of 45 total results (page 1): raise --limit or use --page 2", }, + { + name: "a: capped total on page 1 is a floor", + cmd: pagedCmd(), + pg: listPage{Count: 100, Page: 1, Base: pageOne, Limit: 100, Total: 1000, More: moreYes, TotalCapped: true}, + wantFooter: "Showing 100 results (page 1, total at least 1000).", + wantNote: "note: showing 100 of at least 1000 total results (page 1): raise --limit or use --page 2", + }, + { + name: "a: capped total past the cap, has-more true", + cmd: pagedCmd(), + pg: listPage{Count: 39, Page: 11, Base: pageOne, Limit: 100, Total: 1000, More: moreYes, TotalCapped: true}, + wantFooter: "Showing 39 results (page 11, total at least 1000).", + wantNote: "note: showing 39 of at least 1000 total results (page 11): raise --limit or use --page 12", + }, + { + name: "a: capped total past the cap, has-more false is the end", + cmd: pagedCmd(), + pg: listPage{Count: 100, Page: 11, Base: pageOne, Limit: 100, Total: 1000, More: moreNo, TotalCapped: true}, + wantFooter: "Showing 100 results (page 11, total at least 1000).", + }, + { + name: "a: capped total, has-more false beats the page offset", + cmd: pagedCmd(), + pg: listPage{Count: 100, Page: 1, Base: pageOne, Limit: 100, Total: 1000, More: moreNo, TotalCapped: true}, + wantFooter: "Showing 100 results (page 1, total at least 1000).", + }, + { + name: "a: capped endpoint, a total below the cap is exact", + cmd: pagedCmd(), + pg: listPage{Count: 100, Page: 1, Base: pageOne, Limit: 100, Total: 999, More: moreYes, TotalCapped: true}, + wantFooter: "Showing 100 results (page 1, total 999).", + wantNote: "note: showing 100 of 999 total results (page 1): raise --limit or use --page 2", + }, + { + name: "a: uncapped endpoint, a total of 1000 is exact", + cmd: pagedCmd(), + pg: listPage{Count: 100, Page: 10, Base: pageOne, Limit: 100, Total: 1000}, + wantFooter: "Showing 100 results (page 10, total 1000).", + }, { name: "b: has-more true, singular noun", cmd: pagedCmd(),