Add generated help and version commands

This commit is contained in:
2026-10-07 13:37:57 +00:00
parent c3448a2ebe
commit ecd2b19783
13 changed files with 736 additions and 27 deletions

View File

@@ -5,6 +5,7 @@ import (
"context"
"errors"
"io"
"strconv"
"strings"
"testing"
@@ -250,6 +251,156 @@ func TestExecuteHelpSkipsRequiredInputsAndRejectsNilContext(t *testing.T) {
}
}
func TestHelpRendersRegisteredMetadataAndResolvesExactPaths(t *testing.T) {
type input struct {
count commandkit.Supplied[int]
tags commandkit.Supplied[[]string]
}
factoryCalls, runCalls := 0, 0
show, err := commandkit.NewCommand(commandkit.CommandSpec[input]{
Name: "show",
Summary: "Show one item.",
Description: "Display the selected item in detail.",
NewInput: func() input {
factoryCalls++
return input{}
},
Flags: []commandkit.Flag[input]{
commandkit.ValueFlag(commandkit.FlagMeta{Name: "count", Usage: "number of items", Required: true}, nil, func(value string) (int, error) {
return strconv.Atoi(value)
}, func(target *input, value commandkit.Supplied[int]) { target.count = value }),
commandkit.RepeatFlag(commandkit.FlagMeta{Name: "tag", Usage: "item tag"}, []string{"base"}, func(value string) (string, error) { return value, nil }, func(target *input, value commandkit.Supplied[[]string]) { target.tags = value }),
},
Arguments: []commandkit.Argument[input]{commandkit.RepeatArgument(commandkit.ArgumentMeta{Name: "ID", Usage: "item identifier"}, 1, 3, func(value string) (string, error) { return value, nil }, func(*input, commandkit.Supplied[[]string]) {})},
Run: func(commandkit.Invocation, input) commandkit.Outcome { runCalls++; return commandkit.Success() },
})
if err != nil {
t.Fatal(err)
}
config, err := commandkit.Group("config", "Inspect configuration.", show)
if err != nil {
t.Fatal(err)
}
now, err := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "now", Summary: "Deploy immediately.", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return commandkit.Success() }})
if err != nil {
t.Fatal(err)
}
deployParent, err := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "deploy", Summary: "Deploy artifacts.", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { runCalls++; return commandkit.Success() }})
if err != nil {
t.Fatal(err)
}
deploy, err := commandkit.WithChildren(deployParent, now)
if err != nil {
t.Fatal(err)
}
app, err := commandkit.New(commandkit.Identity{Name: "example"}, config, deploy)
if err != nil {
t.Fatal(err)
}
var out, diagnostic bytes.Buffer
root := app.Execute(context.Background(), []string{"--help"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if root.Kind != commandkit.OutcomeSuccess || !strings.Contains(out.String(), "Available Commands:") {
t.Fatalf("root help = %#v, output %q", root, out.String())
}
for _, name := range []string{"config", "help", "version"} {
if !strings.Contains(out.String(), name) {
t.Fatalf("root help omitted %q: %q", name, out.String())
}
}
for _, earlier := range []string{"config", "deploy", "help"} {
if strings.Index(out.String(), earlier) > strings.Index(out.String(), "version") {
t.Fatalf("root help commands are not sorted: %q", out.String())
}
}
out.Reset()
bare := app.Execute(context.Background(), nil, commandkit.Streams{Out: &out, Err: &diagnostic})
if bare.Kind != commandkit.OutcomeSuccess || !strings.Contains(out.String(), "Available Commands:") {
t.Fatalf("bare root help = %#v, output %q", bare, out.String())
}
out.Reset()
helpCommand := app.Execute(context.Background(), []string{"help"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if helpCommand.Kind != commandkit.OutcomeSuccess || !strings.Contains(out.String(), "Available Commands:") {
t.Fatalf("root help command = %#v, output %q", helpCommand, out.String())
}
out.Reset()
group := app.Execute(context.Background(), []string{"config"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if group.Kind != commandkit.OutcomeSuccess || !strings.Contains(out.String(), "show") {
t.Fatalf("group help = %#v, output %q", group, out.String())
}
out.Reset()
parentHelp := app.Execute(context.Background(), []string{"deploy", "--help"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if parentHelp.Kind != commandkit.OutcomeSuccess || !strings.Contains(out.String(), "now") || strings.Contains(out.String(), "Usage: example deploy now") {
t.Fatalf("runnable parent help = %#v, output %q", parentHelp, out.String())
}
out.Reset()
selected := app.Execute(context.Background(), []string{"help", "config", "show"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if selected.Kind != commandkit.OutcomeSuccess {
t.Fatalf("selected help = %#v, error %q", selected, diagnostic.String())
}
for _, expected := range []string{"Display the selected item in detail.", "Usage:", "ID{1..3}", "Arguments:", "min 1, max 3", "--count", "required", "default: 0", "--tag", "repeatable", "default: [base]", "--help"} {
if !strings.Contains(out.String(), expected) {
t.Fatalf("selected help omitted %q: %q", expected, out.String())
}
}
if factoryCalls != 0 || runCalls != 0 {
t.Fatalf("help invoked execution callbacks: factories=%d runs=%d", factoryCalls, runCalls)
}
badValue := app.Execute(context.Background(), []string{"config", "show", "--count=bad", "--help"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if badValue.Kind != commandkit.OutcomeUsage || factoryCalls != 0 {
t.Fatalf("help skipped conversion of supplied values: %#v", badValue)
}
out.Reset()
versionOutcome := app.Execute(context.Background(), []string{"version"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if versionOutcome.Kind != commandkit.OutcomeSuccess || factoryCalls != 0 || runCalls != 0 {
t.Fatalf("version invoked an application command: %#v calls=%d/%d", versionOutcome, factoryCalls, runCalls)
}
failed := app.Execute(context.Background(), []string{"config", "show", "--help"}, commandkit.Streams{Out: shortWriter{}, Err: &diagnostic})
if failed.Kind != commandkit.OutcomeOperational || !failed.ReportIncomplete || !errors.Is(failed.DeliveryError, io.ErrShortWrite) {
t.Fatalf("failed help delivery = %#v", failed)
}
out.Reset()
unknown := app.Execute(context.Background(), []string{"help", "config", "missing"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if unknown.Kind != commandkit.OutcomeUsage || !strings.Contains(diagnostic.String(), "unknown help path") {
t.Fatalf("unknown help path = %#v, diagnostics %q", unknown, diagnostic.String())
}
badFlag := app.Execute(context.Background(), []string{"config", "show", "--unknown", "--help"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if badFlag.Kind != commandkit.OutcomeUsage {
t.Fatalf("help accepted an unknown flag: %#v", badFlag)
}
}
func TestVersionCommandFormatsApplicationAndCommandKitIdentity(t *testing.T) {
app, err := commandkit.New(commandkit.Identity{Name: "ExampleApp", Revision: "abc123"})
if err != nil {
t.Fatal(err)
}
var out, diagnostic bytes.Buffer
textOutcome := app.Execute(context.Background(), []string{"version"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if textOutcome.Kind != commandkit.OutcomeSuccess || out.String() != "ExampleApp dev (abc123)\n" {
t.Fatalf("default version = %#v, output %q", textOutcome, out.String())
}
out.Reset()
yamlOutcome := app.Execute(context.Background(), []string{"version", "--format=yaml"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if yamlOutcome.Kind != commandkit.OutcomeSuccess || out.String() != "name: ExampleApp\nversion: dev\nrevision: abc123\n" {
t.Fatalf("YAML version = %#v, output %q", yamlOutcome, out.String())
}
out.Reset()
component := app.Execute(context.Background(), []string{"version", "--scope=commandkit"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if component.Kind != commandkit.OutcomeSuccess || !strings.HasPrefix(out.String(), "CommandKit ") || !strings.HasSuffix(out.String(), "\n") {
t.Fatalf("CommandKit version = %#v, output %q", component, out.String())
}
out.Reset()
invalid := app.Execute(context.Background(), []string{"version", "--scope=unknown"}, commandkit.Streams{Out: &out, Err: &diagnostic})
if invalid.Kind != commandkit.OutcomeUsage || out.Len() != 0 {
t.Fatalf("invalid version scope = %#v, output %q", invalid, out.String())
}
failed := app.Execute(context.Background(), []string{"version"}, commandkit.Streams{Out: shortWriter{}, Err: &diagnostic})
if failed.Kind != commandkit.OutcomeOperational || !failed.ReportIncomplete || !errors.Is(failed.DeliveryError, io.ErrShortWrite) {
t.Fatalf("failed version delivery = %#v", failed)
}
}
type failingWriter struct{ err error }
func (w failingWriter) Write([]byte) (int, error) { return 0, w.err }