|
| 1 | +package cli |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "strings" |
| 6 | + "testing" |
| 7 | +) |
| 8 | + |
| 9 | +// TestGenPositionalUseLine asserts the generated --help Use line carries the |
| 10 | +// positional placeholder: an *_ids array op renders the singular id followed by |
| 11 | +// the variadic marker; an *_id scalar op renders the single id; the override and |
| 12 | +// int cases render their pinned field. |
| 13 | +func TestGenPositionalUseLine(t *testing.T) { |
| 14 | + // (a) and (b) read the generated constructors directly (the curated commands |
| 15 | + // own the live `incident ack`/`incident info` path-names, so the generated |
| 16 | + // twins are dropped at registration but still constructible for assertion). |
| 17 | + ack := genIncidentsAckCmd() |
| 18 | + if got := ack.Use; got != "ack <incident-id> [<id2>...]" { |
| 19 | + t.Errorf("ack twin Use = %q, want %q", got, "ack <incident-id> [<id2>...]") |
| 20 | + } |
| 21 | + if ack.Args == nil { |
| 22 | + t.Errorf("ack twin has no Args validator") |
| 23 | + } |
| 24 | + if err := ack.Args(ack, nil); err == nil { |
| 25 | + t.Errorf("ack twin Args accepted zero args (want >=1)") |
| 26 | + } |
| 27 | + if err := ack.Args(ack, []string{"id1"}); err != nil { |
| 28 | + t.Errorf("ack twin Args rejected one arg: %v", err) |
| 29 | + } |
| 30 | + |
| 31 | + info := genIncidentsInfoCmd() |
| 32 | + if got := info.Use; got != "info <incident-id>" { |
| 33 | + t.Errorf("info twin Use = %q, want %q", got, "info <incident-id>") |
| 34 | + } |
| 35 | + if info.Args == nil { |
| 36 | + t.Errorf("info twin has no Args validator") |
| 37 | + } |
| 38 | + if err := info.Args(info, nil); err == nil { |
| 39 | + t.Errorf("info twin Args accepted zero args (want exactly one)") |
| 40 | + } |
| 41 | + if err := info.Args(info, []string{"id1"}); err != nil { |
| 42 | + t.Errorf("info twin Args rejected one arg: %v", err) |
| 43 | + } |
| 44 | + |
| 45 | + // Override cases: merge pins target_incident_id (NOT source_incident_ids); |
| 46 | + // war-room detail pins chat_id. |
| 47 | + merge := genIncidentsMergeCmd() |
| 48 | + if got, want := merge.Use, "merge <target-incident-id>"; got != want { |
| 49 | + t.Errorf("merge override Use = %q, want %q", got, want) |
| 50 | + } |
| 51 | + if strings.Contains(merge.Use, "source-incident") { |
| 52 | + t.Errorf("merge override Use leaked source_incident_ids: %q", merge.Use) |
| 53 | + } |
| 54 | + detail := genIncidentsWarRoomDetailCmd() |
| 55 | + if got, want := detail.Use, "war-room-detail <chat-id>"; got != want { |
| 56 | + t.Errorf("war-room detail override Use = %q, want %q", got, want) |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +// TestGenPositionalScalarStringRuntime invokes a GENERATED-ONLY string-scalar |
| 61 | +// command (`field info <field-id>`) and asserts the positional folds into the |
| 62 | +// request body under the wire key. |
| 63 | +func TestGenPositionalScalarStringRuntime(t *testing.T) { |
| 64 | + saveAndResetGlobals(t) |
| 65 | + stub := newGFStub(t) |
| 66 | + |
| 67 | + if _, err := execCommand("field", "info", "fld-123"); err != nil { |
| 68 | + t.Fatalf("execCommand field info: %v", err) |
| 69 | + } |
| 70 | + if stub.lastPath != "/field/info" { |
| 71 | + t.Fatalf("path = %q, want /field/info", stub.lastPath) |
| 72 | + } |
| 73 | + if got := stub.lastBody["field_id"]; got != "fld-123" { |
| 74 | + t.Errorf("field_id = %#v, want fld-123", got) |
| 75 | + } |
| 76 | +} |
| 77 | + |
| 78 | +// TestGenPositionalSliceRuntime invokes a GENERATED-ONLY string-slice command |
| 79 | +// (`alert list-by-ids <alert-id> [<id2>...]`, whose alert_ids field is []string) |
| 80 | +// and asserts every positional arg folds into the wire array verbatim. |
| 81 | +func TestGenPositionalSliceRuntime(t *testing.T) { |
| 82 | + saveAndResetGlobals(t) |
| 83 | + stub := newGFStub(t) |
| 84 | + |
| 85 | + if _, err := execCommand("alert", "list-by-ids", "a1", "a2", "a3"); err != nil { |
| 86 | + t.Fatalf("execCommand alert list-by-ids: %v", err) |
| 87 | + } |
| 88 | + if stub.lastPath != "/alert/list-by-ids" { |
| 89 | + t.Fatalf("path = %q, want /alert/list-by-ids", stub.lastPath) |
| 90 | + } |
| 91 | + if got, want := fmt.Sprint(stub.bodyStrings("alert_ids")), "[a1 a2 a3]"; got != want { |
| 92 | + t.Errorf("alert_ids = %q, want %q", got, want) |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +// TestGenPositionalIntSliceRuntime invokes a GENERATED-ONLY int-slice command |
| 97 | +// (`team infos <team-id> [<id2>...]`, whose team_ids field is []uint64) and |
| 98 | +// asserts each positional arg is PARSED to an int in the wire array. A raw |
| 99 | +// []string fold (the wrong kind) would fail SDK binding, so this guards the |
| 100 | +// intslice path specifically. |
| 101 | +func TestGenPositionalIntSliceRuntime(t *testing.T) { |
| 102 | + saveAndResetGlobals(t) |
| 103 | + stub := newGFStub(t) |
| 104 | + |
| 105 | + if _, err := execCommand("team", "infos", "11", "22", "33"); err != nil { |
| 106 | + t.Fatalf("execCommand team infos: %v", err) |
| 107 | + } |
| 108 | + if stub.lastPath != "/team/infos" { |
| 109 | + t.Fatalf("path = %q, want /team/infos", stub.lastPath) |
| 110 | + } |
| 111 | + raw, ok := stub.lastBody["team_ids"].([]any) |
| 112 | + if !ok || len(raw) != 3 { |
| 113 | + t.Fatalf("team_ids = %#v, want a 3-element array", stub.lastBody["team_ids"]) |
| 114 | + } |
| 115 | + // JSON numbers decode to float64 through the stub. |
| 116 | + for i, want := range []float64{11, 22, 33} { |
| 117 | + if got, _ := raw[i].(float64); got != want { |
| 118 | + t.Errorf("team_ids[%d] = %#v, want %v", i, raw[i], want) |
| 119 | + } |
| 120 | + } |
| 121 | +} |
| 122 | + |
| 123 | +// TestGenPositionalIntRuntime invokes a GENERATED-ONLY int-scalar command |
| 124 | +// (`schedule info <schedule-id>`) and asserts the positional parses to an int |
| 125 | +// in the wire body (schedule_id is Int64Var, so genFoldPositional uses ParseInt). |
| 126 | +func TestGenPositionalIntRuntime(t *testing.T) { |
| 127 | + saveAndResetGlobals(t) |
| 128 | + stub := newGFStub(t) |
| 129 | + |
| 130 | + // schedule info also needs --start/--end (relative-time required flags); supply |
| 131 | + // them so the command reaches the wire. The positional is the assertion target. |
| 132 | + if _, err := execCommand("schedule", "info", "4242", "--start", "now", "--end", "now"); err != nil { |
| 133 | + t.Fatalf("execCommand schedule info: %v", err) |
| 134 | + } |
| 135 | + if stub.lastPath != "/schedule/info" { |
| 136 | + t.Fatalf("path = %q, want /schedule/info", stub.lastPath) |
| 137 | + } |
| 138 | + // JSON numbers decode to float64 through the stub. |
| 139 | + if got, _ := stub.lastBody["schedule_id"].(float64); got != 4242 { |
| 140 | + t.Errorf("schedule_id = %#v, want 4242", stub.lastBody["schedule_id"]) |
| 141 | + } |
| 142 | +} |
| 143 | + |
| 144 | +// TestGenPositionalFlagOverridesPositional asserts the overlay order: an |
| 145 | +// explicitly-set typed flag for the same field wins over the positional arg |
| 146 | +// (positional folds first, the changed flag stamps after). |
| 147 | +func TestGenPositionalFlagOverridesPositional(t *testing.T) { |
| 148 | + saveAndResetGlobals(t) |
| 149 | + stub := newGFStub(t) |
| 150 | + |
| 151 | + if _, err := execCommand("field", "info", "fromArg", "--field-id", "fromFlag"); err != nil { |
| 152 | + t.Fatalf("execCommand field info with flag: %v", err) |
| 153 | + } |
| 154 | + if got := stub.lastBody["field_id"]; got != "fromFlag" { |
| 155 | + t.Errorf("field_id = %#v, want fromFlag (explicit flag must override positional)", got) |
| 156 | + } |
| 157 | +} |
| 158 | + |
| 159 | +// TestGenFoldPositional unit-tests the runtime helper across all three kinds. |
| 160 | +func TestGenFoldPositional(t *testing.T) { |
| 161 | + // string scalar → args[0] |
| 162 | + b := map[string]any{} |
| 163 | + if err := genFoldPositional([]string{"abc"}, b, "x_id", "string"); err != nil { |
| 164 | + t.Fatalf("string: %v", err) |
| 165 | + } |
| 166 | + if b["x_id"] != "abc" { |
| 167 | + t.Errorf("string: x_id = %#v, want abc", b["x_id"]) |
| 168 | + } |
| 169 | + |
| 170 | + // string slice → all args |
| 171 | + b = map[string]any{} |
| 172 | + if err := genFoldPositional([]string{"a", "b"}, b, "x_ids", "slice"); err != nil { |
| 173 | + t.Fatalf("slice: %v", err) |
| 174 | + } |
| 175 | + if got, want := fmt.Sprint(b["x_ids"]), "[a b]"; got != want { |
| 176 | + t.Errorf("slice: x_ids = %q, want %q", got, want) |
| 177 | + } |
| 178 | + |
| 179 | + // int slice → each arg parsed to int64 |
| 180 | + b = map[string]any{} |
| 181 | + if err := genFoldPositional([]string{"1", "2"}, b, "x_ids", "intslice"); err != nil { |
| 182 | + t.Fatalf("intslice: %v", err) |
| 183 | + } |
| 184 | + if got, want := fmt.Sprint(b["x_ids"]), "[1 2]"; got != want { |
| 185 | + t.Errorf("intslice: x_ids = %q, want %q", got, want) |
| 186 | + } |
| 187 | + if _, ok := b["x_ids"].([]int64); !ok { |
| 188 | + t.Errorf("intslice: x_ids type = %T, want []int64", b["x_ids"]) |
| 189 | + } |
| 190 | + |
| 191 | + // int slice with non-numeric arg → clean error |
| 192 | + b = map[string]any{} |
| 193 | + if err := genFoldPositional([]string{"1", "x"}, b, "x_ids", "intslice"); err == nil { |
| 194 | + t.Errorf("intslice: expected error on non-numeric arg, got nil") |
| 195 | + } |
| 196 | + |
| 197 | + // int scalar → ParseInt |
| 198 | + b = map[string]any{} |
| 199 | + if err := genFoldPositional([]string{"77"}, b, "x_id", "int"); err != nil { |
| 200 | + t.Fatalf("int: %v", err) |
| 201 | + } |
| 202 | + if b["x_id"] != int64(77) { |
| 203 | + t.Errorf("int: x_id = %#v, want int64(77)", b["x_id"]) |
| 204 | + } |
| 205 | + |
| 206 | + // int scalar with non-numeric arg → clean error |
| 207 | + b = map[string]any{} |
| 208 | + if err := genFoldPositional([]string{"nope"}, b, "x_id", "int"); err == nil { |
| 209 | + t.Errorf("int: expected error on non-numeric arg, got nil") |
| 210 | + } |
| 211 | +} |
0 commit comments