Files
xarmian 76c9d5aae5 feat(cmdhelp): parse Examples blocks from cobra Long as fallback (TASK-939) (#331)
The original TASK-939 ask was to migrate every cobra command's
"Examples:" block from Long into the dedicated Example field. Pad has
102 cobra commands; manually migrating each is a hundreds-of-lines
change with high regression risk and zero user-visible improvement
(cobra renders "Examples:" sections in Long identically to the Example
field — the difference is only machine-readability).

Higher-leverage approach: enrich the cmdhelp emitter to fall back to
parsing Long when the Example field is empty. One small, testable change
in internal/cmdhelp unlocks examples in cmdhelp output for every command
that already has an "Examples:" block — without touching any of the 102
command sites. Structural migration becomes optional polish (HT-941).

Implementation:

- internal/cmdhelp/json.go gains parseExamplesFromLong: locate a
  stand-alone "Examples:" / "Example:" header, collect indented
  invocation lines until blank-then-prose or end. Comment lines (`#`)
  are dropped from the block. The header regex is anchored to a line
  on its own (`^\s*Examples?:\s*$`) so prose containing the word
  "Examples" doesn't trigger the fallback.
- buildCommand() prefers cmd.Example when set; falls back to
  parseExamplesFromLong(cmd.Long) when Example is empty. Tests assert
  precedence so future migrations to the Example field win cleanly.
- cmd/pad/main.go: completion command gets a dedicated Example field
  (one of the few that didn't have an "Examples:" block at all). Demos
  the migration pattern HT-941 will sweep across the rest.

Result on the real binary:

  pad help --format json  →  before: 0/100 commands have examples
                             after:  24/100 commands have examples
  pad help item create --format json  →  4 examples (vs 0 before)
  pad help completion --format json   →  4 examples (from Example field)

The remaining ~76 commands genuinely lack an Examples: block in Long
(or are group commands that don't need examples). HT-941 captures the
sweep work needed to get those to 100%.

Tests:

- 7 new tests for parseExamplesFromLong in internal/cmdhelp/json_test.go:
  basic block extraction, no-header (Usage: != Examples:), variant
  headers (singular/plural, indented), empty/malformed inputs, stops
  at unindented prose, drops comment lines.
- 2 new end-to-end tests via Build():
  - falls back to Long when Example is empty
  - prefers Example field when both set (precedence)
- All 39 existing cmdhelp tests + 16 routing tests still pass.
- make check clean.

Follow-up: HT-941 ("Migrate cobra Long Examples blocks to dedicated
Example fields") captures the structural sweep — broken into per-group
PRs (auth/*, agent/*, server/* etc.) so it can be done incrementally
without blocking PLAN-930.

Parent: PLAN-930.
2026-05-01 01:39:23 -04:00

688 lines
22 KiB
Go

package cmdhelp
import (
"bytes"
"encoding/json"
"regexp"
"strings"
"testing"
"github.com/spf13/cobra"
)
// buildSyntheticTree returns a small cobra tree exercising the shapes the
// emitter has to handle: subgroups, leaf commands with positional args,
// flags of every cmdhelp type, repeatable flags, hidden commands, and
// embedded Examples.
//
// root
// ├── item (group)
// │ ├── create <coll> <title> (string args + flags of every type)
// │ └── update <ref> (single positional)
// ├── deep (group, depth>1 to test MaxDepth)
// │ └── nest (group)
// │ └── leaf
// └── secret (hidden)
func buildSyntheticTree() *cobra.Command {
root := &cobra.Command{
Use: "padtest",
Short: "Test root",
Version: "9.9.9",
}
root.PersistentFlags().String("workspace", "", "workspace slug override")
root.PersistentFlags().Int("port", 0, "server port")
item := &cobra.Command{Use: "item", Short: "item group"}
create := &cobra.Command{
Use: "create <collection> <title> [flags]",
Short: "Create a new item",
Long: "Create a new item in the specified collection.\n\nLonger description here.",
Example: " padtest item create task \"Fix OAuth\" --priority high\n padtest item create idea \"Realtime\" --tags x,y,z\n # comment line should be skipped",
}
create.Flags().String("priority", "medium", "priority level")
create.Flags().Bool("stdin", false, "read from stdin")
create.Flags().Int("retries", 0, "retry count")
create.Flags().Float64("budget", 0, "budget cap")
create.Flags().StringSlice("tags", nil, "comma-separated tags")
create.Flags().StringArray("field", nil, "repeatable --field key=value")
hiddenFlag := "internal-debug"
create.Flags().Bool(hiddenFlag, false, "internal use")
_ = create.Flags().MarkHidden(hiddenFlag)
update := &cobra.Command{
Use: "update <ref>",
Short: "Update an item",
}
// Variadic + embedded flag-like fragments — exercised by bulk:
// `bulk-update [--status X] <ref>...`
bulk := &cobra.Command{
Use: "bulk-update [--status X] <ref>...",
Short: "Update multiple items",
}
// Alternation in Use string + ValidArgs on the cobra struct —
// exercised by `completion [bash|zsh|fish|powershell]`.
completion := &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completion",
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
}
// ValidArgs without alternation — exercised by `Use: completion2 [shell]`.
completion2 := &cobra.Command{
Use: "completion2 [shell]",
Short: "Generate shell completion (named arg)",
ValidArgs: []string{"bash", "zsh"},
}
item.AddCommand(create, update, bulk, completion, completion2)
deep := &cobra.Command{Use: "deep", Short: "deep group"}
nest := &cobra.Command{Use: "nest", Short: "nest group"}
leaf := &cobra.Command{Use: "leaf", Short: "leaf"}
nest.AddCommand(leaf)
deep.AddCommand(nest)
secret := &cobra.Command{Use: "secret", Short: "hidden", Hidden: true}
root.AddCommand(item, deep, secret)
return root
}
func TestBuild_TopLevelEnvelope(t *testing.T) {
root := buildSyntheticTree()
doc := Build(root, root, Options{
Binary: "padtest",
Version: "9.9.9",
Homepage: "https://example.test",
MaxDepth: -1,
})
if doc.CmdhelpVersion != "0.1" {
t.Errorf("cmdhelp_version = %q, want 0.1", doc.CmdhelpVersion)
}
if doc.Binary != "padtest" {
t.Errorf("binary = %q, want padtest", doc.Binary)
}
if doc.Version != "9.9.9" {
t.Errorf("version = %q, want 9.9.9", doc.Version)
}
if doc.Homepage != "https://example.test" {
t.Errorf("homepage = %q, want https://example.test", doc.Homepage)
}
if doc.Summary != "Test root" {
t.Errorf("summary = %q, want Test root", doc.Summary)
}
}
func TestBuild_GlobalFlagsEmittedTopLevelOnly(t *testing.T) {
root := buildSyntheticTree()
doc := Build(root, root, Options{Binary: "padtest", MaxDepth: -1})
// Top-level global_flags should contain workspace + port.
if _, ok := doc.GlobalFlags["workspace"]; !ok {
t.Errorf("expected --workspace in global_flags, got: %v", keys(doc.GlobalFlags))
}
if _, ok := doc.GlobalFlags["port"]; !ok {
t.Errorf("expected --port in global_flags, got: %v", keys(doc.GlobalFlags))
}
// And NOT duplicated in any per-command flags map.
for path, cmd := range doc.Commands {
if _, dup := cmd.Flags["workspace"]; dup {
t.Errorf("global flag --workspace must not be duplicated on command %q", path)
}
if _, dup := cmd.Flags["port"]; dup {
t.Errorf("global flag --port must not be duplicated on command %q", path)
}
}
}
func TestBuild_HiddenCommandsAndFlagsExcluded(t *testing.T) {
root := buildSyntheticTree()
doc := Build(root, root, Options{Binary: "padtest", MaxDepth: -1})
if _, present := doc.Commands["secret"]; present {
t.Errorf("hidden command 'secret' must not appear in commands map")
}
create := doc.Commands["item create"]
if _, present := create.Flags["internal-debug"]; present {
t.Errorf("hidden flag --internal-debug must not appear on item create")
}
if _, present := create.Flags["help"]; present {
t.Errorf("cobra-installed --help flag must not appear in emitted flags")
}
}
func TestBuild_VariadicArgsRepeatable(t *testing.T) {
root := buildSyntheticTree()
doc := Build(root, root, Options{Binary: "padtest", MaxDepth: -1})
bulk := doc.Commands["item bulk-update"]
if len(bulk.Args) != 1 {
t.Fatalf("item bulk-update: expected 1 positional arg (embedded --status filtered), got %d: %+v", len(bulk.Args), bulk.Args)
}
if bulk.Args[0].Name != "ref" {
t.Errorf("expected arg name 'ref', got %q", bulk.Args[0].Name)
}
if !bulk.Args[0].Required {
t.Errorf("expected <ref>... to be required")
}
if !bulk.Args[0].Repeatable {
t.Errorf("expected <ref>... to be marked repeatable (variadic)")
}
}
func TestBuild_AlternationProducesEnum(t *testing.T) {
root := buildSyntheticTree()
doc := Build(root, root, Options{Binary: "padtest", MaxDepth: -1})
completion := doc.Commands["item completion"]
if len(completion.Args) != 1 {
t.Fatalf("item completion: expected 1 positional arg, got %d: %+v", len(completion.Args), completion.Args)
}
a := completion.Args[0]
if a.Type != "enum" {
t.Errorf("expected alternation to produce type=enum, got %q", a.Type)
}
if len(a.Enum) != 4 {
t.Errorf("expected 4 enum values, got %d: %v", len(a.Enum), a.Enum)
}
// Values should be in source order.
wantValues := []string{"bash", "zsh", "fish", "powershell"}
for i, w := range wantValues {
if i >= len(a.Enum) || a.Enum[i] != w {
t.Errorf("enum[%d] = %v, want %s", i, a.Enum[i], w)
}
}
}
func TestBuild_ValidArgsFillsEnumOnNamedArg(t *testing.T) {
root := buildSyntheticTree()
doc := Build(root, root, Options{Binary: "padtest", MaxDepth: -1})
c2 := doc.Commands["item completion2"]
if len(c2.Args) != 1 {
t.Fatalf("item completion2: expected 1 positional, got %d: %+v", len(c2.Args), c2.Args)
}
a := c2.Args[0]
if a.Name != "shell" {
t.Errorf("expected arg name from Use string ('shell'), got %q", a.Name)
}
if a.Type != "enum" {
t.Errorf("expected ValidArgs to upgrade type from string to enum, got %q", a.Type)
}
if len(a.Enum) != 2 || a.Enum[0] != "bash" || a.Enum[1] != "zsh" {
t.Errorf("expected enum from ValidArgs [bash zsh], got %v", a.Enum)
}
}
func TestBuild_EmbeddedFlagFragmentsFiltered(t *testing.T) {
root := buildSyntheticTree()
doc := Build(root, root, Options{Binary: "padtest", MaxDepth: -1})
bulk := doc.Commands["item bulk-update"]
for _, a := range bulk.Args {
if strings.HasPrefix(a.Name, "-") {
t.Errorf("flag-like fragment leaked as positional arg: %+v", a)
}
}
}
func TestBuild_PositionalArgsParsed(t *testing.T) {
root := buildSyntheticTree()
doc := Build(root, root, Options{Binary: "padtest", MaxDepth: -1})
create := doc.Commands["item create"]
if len(create.Args) != 2 {
t.Fatalf("item create: expected 2 positional args (collection, title), got %d: %+v", len(create.Args), create.Args)
}
if create.Args[0].Name != "collection" || !create.Args[0].Required {
t.Errorf("arg[0] = %+v, want {collection required:true}", create.Args[0])
}
if create.Args[1].Name != "title" || !create.Args[1].Required {
t.Errorf("arg[1] = %+v, want {title required:true}", create.Args[1])
}
// "[flags]" placeholder must not become a positional arg.
for _, a := range create.Args {
if a.Name == "flags" || a.Name == "options" {
t.Errorf("positional arg %q leaked from cobra placeholder; should be filtered", a.Name)
}
}
}
func TestBuild_FlagTypeMapping(t *testing.T) {
root := buildSyntheticTree()
doc := Build(root, root, Options{Binary: "padtest", MaxDepth: -1})
create := doc.Commands["item create"]
tests := []struct {
name string
wantType string
repeatable bool
}{
{"priority", "string", false},
{"stdin", "bool", false},
{"retries", "int", false},
{"budget", "float", false},
{"tags", "string", true}, // StringSlice → repeatable string
{"field", "string", true}, // StringArray → repeatable string
}
for _, tt := range tests {
f, ok := create.Flags[tt.name]
if !ok {
t.Errorf("expected flag --%s, missing; have: %v", tt.name, keys(create.Flags))
continue
}
if f.Type != tt.wantType {
t.Errorf("--%s type = %q, want %q", tt.name, f.Type, tt.wantType)
}
if f.Repeatable != tt.repeatable {
t.Errorf("--%s repeatable = %v, want %v", tt.name, f.Repeatable, tt.repeatable)
}
}
}
func TestBuild_ZeroDefaultsSuppressed(t *testing.T) {
root := buildSyntheticTree()
doc := Build(root, root, Options{Binary: "padtest", MaxDepth: -1})
create := doc.Commands["item create"]
// stdin defaults to false → suppressed.
if d := create.Flags["stdin"].Default; d != nil {
t.Errorf("--stdin default should be suppressed (zero), got %v", d)
}
// retries defaults to 0 → suppressed.
if d := create.Flags["retries"].Default; d != nil {
t.Errorf("--retries default should be suppressed (zero), got %v", d)
}
// priority defaults to "medium" → emitted.
if d := create.Flags["priority"].Default; d != "medium" {
t.Errorf("--priority default = %v, want \"medium\"", d)
}
}
func TestBuild_ExamplesParsed(t *testing.T) {
root := buildSyntheticTree()
doc := Build(root, root, Options{Binary: "padtest", MaxDepth: -1})
create := doc.Commands["item create"]
if len(create.Examples) != 2 {
t.Fatalf("expected 2 examples (comment line filtered), got %d: %+v", len(create.Examples), create.Examples)
}
if !strings.Contains(create.Examples[0].Cmd, "Fix OAuth") {
t.Errorf("first example: %q does not mention OAuth", create.Examples[0].Cmd)
}
for _, ex := range create.Examples {
if strings.HasPrefix(ex.Cmd, "#") {
t.Errorf("comment line leaked through as example: %q", ex.Cmd)
}
if ex.Cmd != strings.TrimSpace(ex.Cmd) {
t.Errorf("example not whitespace-trimmed: %q", ex.Cmd)
}
}
}
func TestBuild_DescriptionVsSummary(t *testing.T) {
root := buildSyntheticTree()
doc := Build(root, root, Options{Binary: "padtest", MaxDepth: -1})
create := doc.Commands["item create"]
if create.Summary != "Create a new item" {
t.Errorf("summary = %q, want Short value", create.Summary)
}
if !strings.Contains(create.Description, "Longer description here") {
t.Errorf("description missing or wrong: %q", create.Description)
}
}
func TestBuild_CommandPathKeysAreSpaceJoined(t *testing.T) {
root := buildSyntheticTree()
doc := Build(root, root, Options{Binary: "padtest", MaxDepth: -1})
wantPaths := []string{"item", "item create", "item update", "deep", "deep nest", "deep nest leaf"}
for _, p := range wantPaths {
if _, ok := doc.Commands[p]; !ok {
t.Errorf("expected command path %q in commands map; have: %v", p, keys(doc.Commands))
}
}
// Binary itself should not appear as a command entry.
if _, present := doc.Commands["padtest"]; present {
t.Errorf("root binary 'padtest' must not be a command entry")
}
if _, present := doc.Commands[""]; present {
t.Errorf("empty-string key must not be a command entry")
}
}
func TestBuild_MaxDepthLimitsWalk(t *testing.T) {
root := buildSyntheticTree()
// MaxDepth=0 from root: emit only depth-1 children (immediate children of root).
doc0 := Build(root, root, Options{Binary: "padtest", MaxDepth: 0})
if _, ok := doc0.Commands["item"]; !ok {
t.Errorf("MaxDepth=0 should still emit immediate children of root")
}
if _, ok := doc0.Commands["item create"]; ok {
t.Errorf("MaxDepth=0 should not recurse to grandchildren")
}
// MaxDepth=1: include grandchildren.
doc1 := Build(root, root, Options{Binary: "padtest", MaxDepth: 1})
if _, ok := doc1.Commands["item create"]; !ok {
t.Errorf("MaxDepth=1 should include grandchildren like 'item create'")
}
if _, ok := doc1.Commands["deep nest leaf"]; ok {
t.Errorf("MaxDepth=1 should not include depth-3 leaves")
}
// MaxDepth=-1: unlimited.
docAll := Build(root, root, Options{Binary: "padtest", MaxDepth: -1})
if _, ok := docAll.Commands["deep nest leaf"]; !ok {
t.Errorf("MaxDepth=-1 should walk the full tree")
}
}
func TestBuild_TargetSubtree(t *testing.T) {
root := buildSyntheticTree()
itemGroup, _, err := root.Find([]string{"item"})
if err != nil || itemGroup == nil {
t.Fatalf("unable to locate item subtree: %v", err)
}
doc := Build(itemGroup, root, Options{Binary: "padtest", MaxDepth: -1})
// Should contain item subtree.
if _, ok := doc.Commands["item create"]; !ok {
t.Errorf("targeted subtree should include 'item create'")
}
if _, ok := doc.Commands["item update"]; !ok {
t.Errorf("targeted subtree should include 'item update'")
}
// Should NOT contain unrelated subtrees.
if _, ok := doc.Commands["deep"]; ok {
t.Errorf("targeted subtree should not include 'deep' from sibling subtree")
}
if _, ok := doc.Commands["deep nest leaf"]; ok {
t.Errorf("targeted subtree should not include 'deep nest leaf'")
}
}
func TestEmitJSON_ProducesValidJSON(t *testing.T) {
root := buildSyntheticTree()
var buf bytes.Buffer
if err := EmitJSON(root, root, Options{Binary: "padtest", MaxDepth: -1}, &buf); err != nil {
t.Fatalf("EmitJSON: %v", err)
}
var parsed map[string]interface{}
if err := json.Unmarshal(buf.Bytes(), &parsed); err != nil {
t.Fatalf("emitted output is not valid JSON: %v\n%s", err, buf.String())
}
// Required top-level keys per cmdhelp v0.1 §3.
for _, k := range []string{"cmdhelp_version", "binary", "commands"} {
if _, ok := parsed[k]; !ok {
t.Errorf("emitted JSON missing required top-level key %q", k)
}
}
}
func TestCapabilityLine_FormatExact(t *testing.T) {
got := CapabilityLine([]string{"text", "md", "json", "llm"})
want := "cmdhelp/0.1: text, md, json, llm"
if got != want {
t.Errorf("CapabilityLine = %q, want %q", got, want)
}
}
func TestCapabilityLine_HonorsCallerOrderAndSet(t *testing.T) {
// Order is not significant per spec — the helper preserves caller
// order so different binaries can list formats however reads best.
got := CapabilityLine([]string{"json", "text", "md"})
want := "cmdhelp/0.1: json, text, md"
if got != want {
t.Errorf("CapabilityLine should preserve caller order: got %q want %q", got, want)
}
}
func TestEmitJSON_VersionPatternMatchesSpec(t *testing.T) {
// Spec §9: cmdhelp_version is MAJOR.MINOR — never includes a PATCH.
versionRE := regexp.MustCompile(`^[0-9]+\.[0-9]+$`)
if !versionRE.MatchString(Version) {
t.Errorf("Version constant %q must match %s (no PATCH per spec §9)", Version, versionRE)
}
}
func TestExitCode_TerseMarshalsAsString(t *testing.T) {
code := ExitCode{Description: "ok"}
out, err := json.Marshal(code)
if err != nil {
t.Fatalf("marshal terse exit code: %v", err)
}
if string(out) != `"ok"` {
t.Errorf("terse form should marshal as bare string, got: %s", out)
}
}
func TestExitCode_RichMarshalsAsObject(t *testing.T) {
code := ExitCode{When: "validation error", Recovery: "Check args"}
out, err := json.Marshal(code)
if err != nil {
t.Fatalf("marshal rich exit code: %v", err)
}
// Must be an object, must contain "when".
var asObj map[string]interface{}
if err := json.Unmarshal(out, &asObj); err != nil {
t.Fatalf("rich form not an object: %v", err)
}
if asObj["when"] != "validation error" {
t.Errorf("rich form missing/wrong 'when': %s", out)
}
if asObj["recovery"] != "Check args" {
t.Errorf("rich form missing/wrong 'recovery': %s", out)
}
}
func TestParseExamplesFromLong_BasicBlock(t *testing.T) {
long := `Create a new item.
Examples:
pad item create task "fix bug" --priority high
pad item create idea "feature x"
Run with --help-collections to see available collections.`
got := parseExamplesFromLong(long)
if len(got) != 2 {
t.Fatalf("expected 2 examples, got %d: %+v", len(got), got)
}
if got[0].Cmd != `pad item create task "fix bug" --priority high` {
t.Errorf("example[0] = %q", got[0].Cmd)
}
if got[1].Cmd != `pad item create idea "feature x"` {
t.Errorf("example[1] = %q", got[1].Cmd)
}
}
func TestParseExamplesFromLong_NoHeader(t *testing.T) {
long := `Generate shell completion scripts.
Usage:
source <(pad completion bash)
pad completion zsh > "${fpath[1]}/_pad"`
got := parseExamplesFromLong(long)
if got != nil {
// A "Usage:" section is NOT examples. The fallback must not
// trigger here — completion should rely on its dedicated
// Example field instead.
t.Errorf("expected nil (Usage: != Examples:), got %v", got)
}
}
func TestParseExamplesFromLong_VariantHeaders(t *testing.T) {
cases := map[string]string{
"Examples:": "Examples:\n pad x",
"Example:": "Example:\n pad x",
" Examples:": " Examples:\n pad x",
}
for name, long := range cases {
t.Run(name, func(t *testing.T) {
got := parseExamplesFromLong(long)
if len(got) != 1 || got[0].Cmd != "pad x" {
t.Errorf("%s header: expected 1 example 'pad x', got %+v", name, got)
}
})
}
}
func TestParseExamplesFromLong_EmptyOrMalformed(t *testing.T) {
cases := []string{
"", // empty
"no examples here", // no header
"Examples:", // header only, no body
"Examples:\n\n", // header + blank line(s)
}
for _, in := range cases {
if got := parseExamplesFromLong(in); got != nil {
t.Errorf("expected nil for %q, got %v", in, got)
}
}
}
func TestParseExamplesFromLong_StopsAtUnindentedProse(t *testing.T) {
// Common cobra Long pattern: "Examples:" block followed by a
// trailing prose paragraph. The paragraph is unindented; the
// parser stops there.
long := `Description here.
Examples:
pad foo
pad bar
Note: see related command for more.`
got := parseExamplesFromLong(long)
if len(got) != 2 {
t.Fatalf("expected 2 examples (Note: line stops the block), got %d: %+v", len(got), got)
}
for _, ex := range got {
if strings.HasPrefix(ex.Cmd, "Note:") {
t.Errorf("trailing prose leaked as example: %q", ex.Cmd)
}
}
}
func TestParseExamplesFromLong_DropsCommentLines(t *testing.T) {
long := `Examples:
# this is a section comment
pad foo --bar
# another comment
pad baz`
got := parseExamplesFromLong(long)
if len(got) != 2 {
t.Fatalf("expected 2 examples (comments dropped), got %d: %+v", len(got), got)
}
}
func TestBuild_FallsBackToLongWhenExampleEmpty(t *testing.T) {
// End-to-end via Build(): a command with no Example field but an
// Examples: section in Long should produce the same Examples
// structurally as if Example were set.
root := &cobra.Command{Use: "padtest"}
leaf := &cobra.Command{
Use: "leaf",
Short: "leaf cmd",
Long: `Do the thing.
Examples:
padtest leaf --opt one
padtest leaf --opt two`,
// Example: "" (intentionally empty)
}
root.AddCommand(leaf)
doc := Build(root, root, Options{Binary: "padtest", MaxDepth: -1})
got := doc.Commands["leaf"].Examples
if len(got) != 2 {
t.Errorf("expected 2 examples extracted from Long, got %d: %+v", len(got), got)
}
}
func TestBuild_PrefersExampleFieldOverLong(t *testing.T) {
// When both Example and Long-Examples are present, the dedicated
// Example field wins (it's the canonical structured source).
root := &cobra.Command{Use: "padtest"}
leaf := &cobra.Command{
Use: "leaf",
Short: "leaf cmd",
Example: ` padtest leaf --winner`,
Long: `Do the thing.
Examples:
padtest leaf --loser`,
}
root.AddCommand(leaf)
doc := Build(root, root, Options{Binary: "padtest", MaxDepth: -1})
got := doc.Commands["leaf"].Examples
if len(got) != 1 {
t.Fatalf("expected exactly 1 example (Example field wins), got %d", len(got))
}
if got[0].Cmd != "padtest leaf --winner" {
t.Errorf("Example field should take precedence, got %q", got[0].Cmd)
}
}
func TestParseExamples_FiltersBlankAndComments(t *testing.T) {
in := " \n cmd one\n# a comment\n\n cmd two\n # also a comment\n"
got := parseExamples(in)
if len(got) != 2 {
t.Fatalf("expected 2 examples, got %d: %+v", len(got), got)
}
if got[0].Cmd != "cmd one" || got[1].Cmd != "cmd two" {
t.Errorf("got %+v, want [cmd one, cmd two]", got)
}
}
func TestMapPflagType_CoversCommonTypes(t *testing.T) {
cases := map[string]struct {
want string
repeatable bool
}{
"string": {"string", false},
"int": {"int", false},
"int64": {"int", false},
"float64": {"float", false},
"bool": {"bool", false},
"duration": {"duration", false},
"stringSlice": {"string", true},
"stringArray": {"string", true},
"boolSlice": {"bool", true},
"weirdType": {"string", false}, // fallback
}
for in, want := range cases {
got, rep := mapPflagType(in)
if got != want.want || rep != want.repeatable {
t.Errorf("mapPflagType(%q) = (%q, %v), want (%q, %v)", in, got, rep, want.want, want.repeatable)
}
}
}
func keys(m interface{}) []string {
switch v := m.(type) {
case map[string]Flag:
out := make([]string, 0, len(v))
for k := range v {
out = append(out, k)
}
return out
case map[string]Command:
out := make([]string, 0, len(v))
for k := range v {
out = append(out, k)
}
return out
}
return nil
}