@@ -1599,6 +1599,167 @@ type readerFunc func([]byte) (int, error)
15991599
16001600func (f readerFunc ) Read (p []byte ) (int , error ) { return f (p ) }
16011601
1602+ // ---------------------------------------------------------------------------
1603+ // Structured list truncation indicator
1604+ // ---------------------------------------------------------------------------
1605+
1606+ // TestCommandAlertListStructuredAnnouncesTruncation pins that a structured
1607+ // list page which doesn't cover the server-reported total says so on stderr:
1608+ // stdout is reserved for the jq/toon pipeline, so without the note a consumer
1609+ // sees the default --limit page as the whole set. Table mode keeps its
1610+ // "Showing N results" footer on stdout and emits no stderr note.
1611+ func TestCommandAlertListStructuredAnnouncesTruncation (t * testing.T ) {
1612+ twoOfFive := map [string ]any {"items" : []any {alertRow (), alertRow ()}, "total" : 5 }
1613+
1614+ t .Run ("json page short of total" , func (t * testing.T ) {
1615+ saveAndResetGlobals (t )
1616+ stub := newGFStub (t )
1617+ stub .data = twoOfFive
1618+
1619+ out , stderrText , err := execCommandSplit ("alert" , "list" , "--output-format" , "json" )
1620+ if err != nil {
1621+ t .Fatalf ("execCommandSplit: %v" , err )
1622+ }
1623+ var rows []map [string ]any
1624+ if err := json .Unmarshal ([]byte (strings .TrimSpace (out )), & rows ); err != nil {
1625+ t .Fatalf ("stdout must stay parseable JSON: %v\n %s" , err , out )
1626+ }
1627+ if len (rows ) != 2 {
1628+ t .Fatalf ("got %d rows, want the 2 the page carries" , len (rows ))
1629+ }
1630+ if ! strings .Contains (stderrText , "note: showing 2 of 5 total results (page 1)" ) {
1631+ t .Errorf ("truncated structured page should announce itself on stderr, got:\n %s" , stderrText )
1632+ }
1633+ })
1634+
1635+ t .Run ("json page covers total" , func (t * testing.T ) {
1636+ saveAndResetGlobals (t )
1637+ stub := newGFStub (t )
1638+ stub .data = map [string ]any {"items" : []any {alertRow (), alertRow ()}, "total" : 2 }
1639+
1640+ _ , stderrText , err := execCommandSplit ("alert" , "list" , "--output-format" , "json" )
1641+ if err != nil {
1642+ t .Fatalf ("execCommandSplit: %v" , err )
1643+ }
1644+ if strings .Contains (stderrText , "note: showing" ) {
1645+ t .Errorf ("a page covering the total must not cry truncation, got:\n %s" , stderrText )
1646+ }
1647+ })
1648+
1649+ t .Run ("table keeps the footer on stdout" , func (t * testing.T ) {
1650+ saveAndResetGlobals (t )
1651+ stub := newGFStub (t )
1652+ stub .data = twoOfFive
1653+
1654+ out , stderrText , err := execCommandSplit ("alert" , "list" )
1655+ if err != nil {
1656+ t .Fatalf ("execCommandSplit: %v" , err )
1657+ }
1658+ if ! strings .Contains (out , "Showing 2 results (page 1, total 5)." ) {
1659+ t .Errorf ("table mode should keep the stdout footer, got:\n %s" , out )
1660+ }
1661+ if strings .Contains (stderrText , "note: showing" ) {
1662+ t .Errorf ("table mode already footers the count; no stderr note wanted, got:\n %s" , stderrText )
1663+ }
1664+ })
1665+
1666+ // The truncation judgment must account for the page offset: page 2 of a
1667+ // total 40 at --limit 20 IS the last page — there is no rest, so no note.
1668+ fullPage := make ([]any , 20 )
1669+ for i := range fullPage {
1670+ fullPage [i ] = alertRow ()
1671+ }
1672+ lastPage := map [string ]any {"items" : fullPage , "total" : 40 }
1673+
1674+ t .Run ("json last page prints no note" , func (t * testing.T ) {
1675+ saveAndResetGlobals (t )
1676+ stub := newGFStub (t )
1677+ stub .data = lastPage
1678+
1679+ _ , stderrText , err := execCommandSplit ("alert" , "list" , "--limit" , "20" , "--page" , "2" , "--output-format" , "json" )
1680+ if err != nil {
1681+ t .Fatalf ("execCommandSplit: %v" , err )
1682+ }
1683+ if strings .Contains (stderrText , "note: showing" ) {
1684+ t .Errorf ("the last page has no rest to page for; no note wanted, got:\n %s" , stderrText )
1685+ }
1686+ })
1687+
1688+ t .Run ("json first page of same total still notes" , func (t * testing.T ) {
1689+ saveAndResetGlobals (t )
1690+ stub := newGFStub (t )
1691+ stub .data = lastPage
1692+
1693+ _ , stderrText , err := execCommandSplit ("alert" , "list" , "--limit" , "20" , "--page" , "1" , "--output-format" , "json" )
1694+ if err != nil {
1695+ t .Fatalf ("execCommandSplit: %v" , err )
1696+ }
1697+ if ! strings .Contains (stderrText , "note: showing 20 of 40 total results (page 1)" ) {
1698+ t .Errorf ("page 1 with a page 2 beyond it should announce itself on stderr, got:\n %s" , stderrText )
1699+ }
1700+ })
1701+ }
1702+
1703+ // ---------------------------------------------------------------------------
1704+ // Projection overflow is a hard failure
1705+ // ---------------------------------------------------------------------------
1706+
1707+ // TestCommandListProjectionOverflowFails pins that a compact list projection
1708+ // which cannot fit the byte budget fails the command instead of emitting
1709+ // anything: Execute returns the error and stdout stays empty, so a pipeline
1710+ // reading stdout sees a failed call, never an empty page masquerading as
1711+ // "no data".
1712+ func TestCommandListProjectionOverflowFails (t * testing.T ) {
1713+ t .Run ("incident list" , func (t * testing.T ) {
1714+ saveAndResetGlobals (t )
1715+ stub := newGFStub (t )
1716+ items := make ([]any , 100 )
1717+ for i := range items {
1718+ row := incidentRow ()
1719+ row ["incident_id" ] = fmt .Sprintf ("inc-%024d" , i )
1720+ row ["title" ] = strings .Repeat ("x" , 200 )
1721+ items [i ] = row
1722+ }
1723+ stub .data = map [string ]any {"items" : items , "total" : len (items )}
1724+
1725+ out , stderrText , err := execCommandSplit ("incident" , "list" , "--limit" , "100" , "--output-format" , "json" )
1726+ if err == nil || ! strings .Contains (err .Error (), "exceeds the 16384-byte limit" ) {
1727+ t .Fatalf ("irreducible projection error = %v, want the byte-limit refusal" , err )
1728+ }
1729+ if out != "" {
1730+ t .Errorf ("a failed projection must write nothing to stdout, got %d bytes" , len (out ))
1731+ }
1732+ if strings .Contains (stderrText , "exceeds the" ) {
1733+ t .Errorf ("the error is returned for the entrypoint to report, not printed mid-run, got:\n %s" , stderrText )
1734+ }
1735+ })
1736+
1737+ t .Run ("alert-event list" , func (t * testing.T ) {
1738+ saveAndResetGlobals (t )
1739+ stub := newGFStub (t )
1740+ items := make ([]any , 100 )
1741+ for i := range items {
1742+ items [i ] = map [string ]any {
1743+ "event_id" : fmt .Sprintf ("%024x" , i ),
1744+ "alert_id" : fmt .Sprintf ("%024x" , i + 1_000_000 ),
1745+ "event_severity" : "Warning" ,
1746+ "event_status" : "Triggered" ,
1747+ "event_time" : 1712000000 + i ,
1748+ "title" : strings .Repeat ("x" , 200 ),
1749+ }
1750+ }
1751+ stub .data = map [string ]any {"items" : items , "total" : len (items )}
1752+
1753+ out , _ , err := execCommandSplit ("alert-event" , "list" , "--limit" , "100" , "--output-format" , "json" )
1754+ if err == nil || ! strings .Contains (err .Error (), "exceeds the 16384-byte limit" ) {
1755+ t .Fatalf ("irreducible projection error = %v, want the byte-limit refusal" , err )
1756+ }
1757+ if out != "" {
1758+ t .Errorf ("a failed projection must write nothing to stdout, got %d bytes" , len (out ))
1759+ }
1760+ })
1761+ }
1762+
16021763// ---------------------------------------------------------------------------
16031764// Helpers
16041765// ---------------------------------------------------------------------------
0 commit comments