@@ -919,3 +919,182 @@ func TestBoundProjectedListErrorNamesLargestFields(t *testing.T) {
919919 t .Fatalf ("list overflow error = %q, want it to name the largest fields" , err )
920920 }
921921}
922+
923+ // escalateRuleRow is a full EscalateRuleItem stub payload, including the
924+ // nested layers/time_filters blobs that bloat the full dump — the same shape
925+ // that made the raw generated command's toon output oversized per rule.
926+ func escalateRuleRow () map [string ]any {
927+ return map [string ]any {
928+ "account_id" : 1001 ,
929+ "aggr_window" : 0 ,
930+ "channel_id" : 4201 ,
931+ "channel_name" : "payments" ,
932+ "created_at" : 1712000000 ,
933+ "deleted_at" : 0 ,
934+ "description" : "page the primary on-call, then the team" ,
935+ "filters" : []any {
936+ []any {map [string ]any {"key" : "incident_severity" , "oper" : "IN" , "vals" : []any {"Critical" }}},
937+ },
938+ "layers" : []any {
939+ map [string ]any {
940+ "escalate_window" : 30 ,
941+ "max_times" : 3 ,
942+ "notify_step" : 5 ,
943+ "target" : map [string ]any {"person_ids" : []any {101 }, "by" : map [string ]any {"critical" : []any {"voice" , "sms" }}},
944+ },
945+ },
946+ "priority" : 10 ,
947+ "rule_id" : "6621b23f4a2c5e0012ab34d0" ,
948+ "rule_name" : "P1 on-call" ,
949+ "status" : "enabled" ,
950+ "template_id" : "6630c34f5b3d6e0012cd45e1" ,
951+ "time_filters" : []any {
952+ map [string ]any {"start" : "09:00" , "end" : "18:00" , "repeat" : []any {1 , 2 , 3 }},
953+ },
954+ "updated_at" : 1712100000 ,
955+ "updated_by" : 101 ,
956+ }
957+ }
958+
959+ // TestChannelEscalateRuleListStructuredProjection mirrors
960+ // TestIncidentListStructuredDefaultUsesCompactProjection for the curated
961+ // channel escalate-rule-list: json/toon mode must default to the compact
962+ // projection (announced on stderr) instead of dumping the full nested rule
963+ // record, an explicit --fields must override it, and table mode keeps its
964+ // explicit columns without the note.
965+ func TestChannelEscalateRuleListStructuredProjection (t * testing.T ) {
966+ t .Run ("json default" , func (t * testing.T ) {
967+ saveAndResetGlobals (t )
968+ stub := newGFStub (t )
969+ stub .data = map [string ]any {"items" : []any {escalateRuleRow ()}}
970+
971+ out , stderrText , err := execCommandSplit ("channel" , "escalate-rule-list" , "4201" , "--output-format" , "json" )
972+ if err != nil {
973+ t .Fatalf ("execCommandSplit: %v" , err )
974+ }
975+
976+ assertProjectedJSONFields (t , out , []string {"rule_id" , "rule_name" , "status" , "priority" , "filters" })
977+ if ! strings .Contains (stderrText , "note: rows projected to default compact fields" ) {
978+ t .Errorf ("default projection should announce itself on stderr, got:\n %s" , stderrText )
979+ }
980+ for _ , key := range []string {"layers" , "template_id" } {
981+ if strings .Contains (out , key ) {
982+ t .Errorf ("default json output should not contain full-record key %q, got:\n %s" , key , out )
983+ }
984+ }
985+ })
986+
987+ t .Run ("toon default" , func (t * testing.T ) {
988+ saveAndResetGlobals (t )
989+ stub := newGFStub (t )
990+ stub .data = map [string ]any {"items" : []any {escalateRuleRow ()}}
991+
992+ out , stderrText , err := execCommandSplit ("channel" , "escalate-rule-list" , "4201" , "--output-format" , "toon" )
993+ if err != nil {
994+ t .Fatalf ("execCommandSplit: %v" , err )
995+ }
996+
997+ // Positive keys must come from stdout alone: the stderr note embeds the
998+ // same field names, so a merged capture would satisfy this vacuously.
999+ for _ , key := range []string {"rule_id" , "rule_name" , "status" , "priority" , "filters" } {
1000+ if ! strings .Contains (out , key ) {
1001+ t .Errorf ("default toon output missing compact key %q, got:\n %s" , key , out )
1002+ }
1003+ }
1004+ if ! strings .Contains (stderrText , "note: rows projected to default compact fields" ) {
1005+ t .Errorf ("default projection should announce itself on stderr, got:\n %s" , stderrText )
1006+ }
1007+ for _ , key := range []string {"layers" , "template_id" , "description" } {
1008+ if strings .Contains (out , key ) {
1009+ t .Errorf ("default toon output should not contain full-record key %q, got:\n %s" , key , out )
1010+ }
1011+ }
1012+ })
1013+
1014+ t .Run ("explicit fields win" , func (t * testing.T ) {
1015+ saveAndResetGlobals (t )
1016+ stub := newGFStub (t )
1017+ stub .data = map [string ]any {"items" : []any {escalateRuleRow ()}}
1018+
1019+ out , err := execCommand ("channel" , "escalate-rule-list" , "4201" , "--fields" , "rule_id,layers" , "--output-format" , "json" )
1020+ if err != nil {
1021+ t .Fatalf ("execCommand: %v" , err )
1022+ }
1023+
1024+ assertProjectedJSONFields (t , out , []string {"rule_id" , "layers" })
1025+ })
1026+
1027+ t .Run ("explicit empty fields errors" , func (t * testing.T ) {
1028+ saveAndResetGlobals (t )
1029+ stub := newGFStub (t )
1030+ stub .data = map [string ]any {"items" : []any {escalateRuleRow ()}}
1031+
1032+ _ , err := execCommand ("channel" , "escalate-rule-list" , "4201" , "--fields" , "" , "--output-format" , "json" )
1033+ if err == nil {
1034+ t .Fatal ("expected an error for empty --fields, got nil" )
1035+ }
1036+ if ! strings .Contains (err .Error (), "--fields" ) {
1037+ t .Errorf ("error should name --fields, got: %v" , err )
1038+ }
1039+ })
1040+
1041+ t .Run ("unknown field errors" , func (t * testing.T ) {
1042+ saveAndResetGlobals (t )
1043+ stub := newGFStub (t )
1044+ stub .data = map [string ]any {"items" : []any {escalateRuleRow ()}}
1045+
1046+ _ , err := execCommand ("channel" , "escalate-rule-list" , "4201" , "--fields" , "not_a_field" , "--output-format" , "json" )
1047+ if err == nil {
1048+ t .Fatal ("expected an error for an unknown field, got nil" )
1049+ }
1050+ if ! strings .Contains (err .Error (), "not_a_field" ) {
1051+ t .Errorf ("error should name the bad field, got: %v" , err )
1052+ }
1053+ })
1054+
1055+ t .Run ("table mode headers without note" , func (t * testing.T ) {
1056+ saveAndResetGlobals (t )
1057+ stub := newGFStub (t )
1058+ stub .data = map [string ]any {"items" : []any {escalateRuleRow ()}}
1059+
1060+ out , stderrText , err := execCommandSplit ("channel" , "escalate-rule-list" , "4201" )
1061+ if err != nil {
1062+ t .Fatalf ("execCommandSplit: %v" , err )
1063+ }
1064+ for _ , h := range []string {"ID" , "NAME" , "STATUS" , "PRIORITY" , "UPDATED" } {
1065+ if ! strings .Contains (out , h ) {
1066+ t .Errorf ("table output missing header %q, got:\n %s" , h , out )
1067+ }
1068+ }
1069+ if strings .Contains (stderrText , "note: rows projected" ) {
1070+ t .Errorf ("table mode must not print the projection note, got:\n %s" , stderrText )
1071+ }
1072+ })
1073+
1074+ t .Run ("channel id folding" , func (t * testing.T ) {
1075+ for _ , tc := range []struct {
1076+ name string
1077+ args []string
1078+ want float64
1079+ }{
1080+ {"positional" , []string {"channel" , "escalate-rule-list" , "4201" }, 4201 },
1081+ {"flag" , []string {"channel" , "escalate-rule-list" , "--channel-id" , "4202" }, 4202 },
1082+ } {
1083+ t .Run (tc .name , func (t * testing.T ) {
1084+ saveAndResetGlobals (t )
1085+ stub := newGFStub (t )
1086+ stub .data = map [string ]any {"items" : []any {}}
1087+
1088+ if _ , err := execCommand (tc .args ... ); err != nil {
1089+ t .Fatalf ("execCommand: %v" , err )
1090+ }
1091+ if stub .lastPath != "/channel/escalate/rule/list" {
1092+ t .Fatalf ("expected /channel/escalate/rule/list, got %q" , stub .lastPath )
1093+ }
1094+ if stub .lastBody ["channel_id" ] != tc .want {
1095+ t .Fatalf ("channel_id = %#v, want %v" , stub .lastBody ["channel_id" ], tc .want )
1096+ }
1097+ })
1098+ }
1099+ })
1100+ }
0 commit comments