feat: Command DTO + Cli surface — AX-native CLI primitives
Command is now a DTO with no root/child awareness:
- Path-based registration: c.Command("deploy/to/homelab", handler)
- Description is an i18n key derived from path: cmd.deploy.to.homelab.description
- Lifecycle: Run(), Start(), Stop(), Restart(), Reload(), Signal()
- All return core.Result — errors flow through Core internally
- Parent commands auto-created from path segments
Cli is now a surface layer that reads from Core's command registry:
- Resolves os.Args to command path
- Parses flags into Options (--port=8080 → Option{K:"port", V:"8080"})
- Calls command action with parsed Options
- Banner and help use i18n
Old Clir code preserved in tests/testdata/cli_clir.go.bak for reference.
211 tests, 77.5% coverage.
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
b2d07e7883
commit
afc235796f
7 changed files with 1828 additions and 1514 deletions
304
pkg/core/cli.go
304
pkg/core/cli.go
|
|
@ -1,200 +1,152 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
// CLI command framework for the Core framework.
|
||||
// Based on leaanthony/clir — zero-dependency command line interface.
|
||||
|
||||
// Cli is the CLI surface layer for the Core command tree.
|
||||
// It reads commands from Core's registry and wires them to terminal I/O.
|
||||
//
|
||||
// Run the CLI:
|
||||
//
|
||||
// c := core.New(core.Options{{K: "name", V: "myapp"}})
|
||||
// c.Command("deploy", handler)
|
||||
// c.Cli().Run()
|
||||
//
|
||||
// The Cli resolves os.Args to a command path, parses flags,
|
||||
// and calls the command's action with parsed options.
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CliAction represents a function called when a command is invoked.
|
||||
type CliAction func() error
|
||||
|
||||
// CliOpts configures a Cli.
|
||||
type CliOpts struct {
|
||||
Version string
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
// Cli is the CLI command framework.
|
||||
// Cli is the CLI surface for the Core command tree.
|
||||
type Cli struct {
|
||||
opts *CliOpts
|
||||
rootCommand *Command
|
||||
defaultCommand *Command
|
||||
preRunCommand func(*Cli) error
|
||||
postRunCommand func(*Cli) error
|
||||
bannerFunction func(*Cli) string
|
||||
errorHandler func(string, error) error
|
||||
core *Core
|
||||
banner func(*Cli) string
|
||||
}
|
||||
|
||||
// defaultBannerFunction prints a banner for the application.
|
||||
func defaultBannerFunction(c *Cli) string {
|
||||
version := ""
|
||||
if c.opts != nil && c.opts.Version != "" {
|
||||
version = " " + c.opts.Version
|
||||
}
|
||||
name := ""
|
||||
description := ""
|
||||
if c.opts != nil {
|
||||
name = c.opts.Name
|
||||
description = c.opts.Description
|
||||
}
|
||||
if description != "" {
|
||||
return fmt.Sprintf("%s%s - %s", name, version, description)
|
||||
}
|
||||
return fmt.Sprintf("%s%s", name, version)
|
||||
}
|
||||
|
||||
|
||||
// Command returns the root command.
|
||||
func (c *Cli) Command() *Command {
|
||||
return c.rootCommand
|
||||
}
|
||||
|
||||
// Version returns the application version string.
|
||||
func (c *Cli) Version() string {
|
||||
if c.opts != nil {
|
||||
return c.opts.Version
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Name returns the application name.
|
||||
func (c *Cli) Name() string {
|
||||
if c.opts != nil {
|
||||
return c.opts.Name
|
||||
}
|
||||
return c.rootCommand.name
|
||||
}
|
||||
|
||||
// ShortDescription returns the application short description.
|
||||
func (c *Cli) ShortDescription() string {
|
||||
if c.opts != nil {
|
||||
return c.opts.Description
|
||||
}
|
||||
return c.rootCommand.shortdescription
|
||||
}
|
||||
|
||||
// SetBannerFunction sets the function that generates the banner string.
|
||||
func (c *Cli) SetBannerFunction(fn func(*Cli) string) {
|
||||
c.bannerFunction = fn
|
||||
}
|
||||
|
||||
// SetErrorFunction sets a custom error handler for undefined flags.
|
||||
func (c *Cli) SetErrorFunction(fn func(string, error) error) {
|
||||
c.errorHandler = fn
|
||||
}
|
||||
|
||||
// AddCommand adds a command to the application.
|
||||
func (c *Cli) AddCommand(command *Command) {
|
||||
c.rootCommand.AddCommand(command)
|
||||
}
|
||||
|
||||
// PrintBanner prints the application banner.
|
||||
func (c *Cli) PrintBanner() {
|
||||
fmt.Println(c.bannerFunction(c))
|
||||
fmt.Println("")
|
||||
}
|
||||
|
||||
// PrintHelp prints the application help.
|
||||
func (c *Cli) PrintHelp() {
|
||||
c.rootCommand.PrintHelp()
|
||||
}
|
||||
|
||||
// Run runs the application with the given arguments.
|
||||
func (c *Cli) Run(args ...string) error {
|
||||
if c.preRunCommand != nil {
|
||||
if err := c.preRunCommand(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Run resolves os.Args to a command path and executes it.
|
||||
//
|
||||
// c.Cli().Run()
|
||||
// c.Cli().Run("deploy", "to", "homelab")
|
||||
func (cl *Cli) Run(args ...string) Result[any] {
|
||||
if len(args) == 0 {
|
||||
args = os.Args[1:]
|
||||
}
|
||||
if err := c.rootCommand.run(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.postRunCommand != nil {
|
||||
if err := c.postRunCommand(c); err != nil {
|
||||
return err
|
||||
|
||||
// Filter out empty args and test flags
|
||||
var clean []string
|
||||
for _, a := range args {
|
||||
if a != "" && !strings.HasPrefix(a, "-test.") {
|
||||
clean = append(clean, a)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
if cl.core == nil || cl.core.commands == nil || len(cl.core.commands.commands) == 0 {
|
||||
// No commands registered — print banner and exit
|
||||
if cl.banner != nil {
|
||||
fmt.Println(cl.banner(cl))
|
||||
}
|
||||
return Result[any]{}
|
||||
}
|
||||
|
||||
// Resolve command path from args
|
||||
// "deploy to homelab" → try "deploy/to/homelab", then "deploy/to", then "deploy"
|
||||
var cmd *Command
|
||||
var remaining []string
|
||||
|
||||
for i := len(clean); i > 0; i-- {
|
||||
path := strings.Join(clean[:i], "/")
|
||||
if c, ok := cl.core.commands.commands[path]; ok {
|
||||
cmd = c
|
||||
remaining = clean[i:]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if cmd == nil {
|
||||
// No matching command — try root-level action or print help
|
||||
if cl.banner != nil {
|
||||
fmt.Println(cl.banner(cl))
|
||||
}
|
||||
cl.PrintHelp()
|
||||
return Result[any]{}
|
||||
}
|
||||
|
||||
// Build options from remaining args (flags become Options)
|
||||
opts := Options{}
|
||||
for _, arg := range remaining {
|
||||
if strings.HasPrefix(arg, "--") {
|
||||
parts := strings.SplitN(strings.TrimPrefix(arg, "--"), "=", 2)
|
||||
if len(parts) == 2 {
|
||||
opts = append(opts, Option{K: parts[0], V: parts[1]})
|
||||
} else {
|
||||
opts = append(opts, Option{K: parts[0], V: true})
|
||||
}
|
||||
} else if strings.HasPrefix(arg, "-") {
|
||||
parts := strings.SplitN(strings.TrimPrefix(arg, "-"), "=", 2)
|
||||
if len(parts) == 2 {
|
||||
opts = append(opts, Option{K: parts[0], V: parts[1]})
|
||||
} else {
|
||||
opts = append(opts, Option{K: parts[0], V: true})
|
||||
}
|
||||
} else {
|
||||
opts = append(opts, Option{K: "_arg", V: arg})
|
||||
}
|
||||
}
|
||||
|
||||
return cmd.Run(opts)
|
||||
}
|
||||
|
||||
// DefaultCommand sets the command to run when no other commands are given.
|
||||
func (c *Cli) DefaultCommand(defaultCommand *Command) *Cli {
|
||||
c.defaultCommand = defaultCommand
|
||||
return c
|
||||
// PrintHelp prints available commands.
|
||||
//
|
||||
// c.Cli().PrintHelp()
|
||||
func (cl *Cli) PrintHelp() {
|
||||
if cl.core == nil || cl.core.commands == nil {
|
||||
return
|
||||
}
|
||||
|
||||
name := ""
|
||||
if cl.core.app != nil {
|
||||
name = cl.core.app.Name
|
||||
}
|
||||
if name != "" {
|
||||
fmt.Printf("%s commands:\n\n", name)
|
||||
} else {
|
||||
fmt.Println("Commands:\n")
|
||||
}
|
||||
|
||||
cl.core.commands.mu.RLock()
|
||||
defer cl.core.commands.mu.RUnlock()
|
||||
|
||||
for path, cmd := range cl.core.commands.commands {
|
||||
if cmd.hidden {
|
||||
continue
|
||||
}
|
||||
desc := cl.core.I18n().T(cmd.I18nKey())
|
||||
// If i18n returned the key itself (no translation), show path only
|
||||
if desc == cmd.I18nKey() {
|
||||
fmt.Printf(" %s\n", path)
|
||||
} else {
|
||||
fmt.Printf(" %-30s %s\n", path, desc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewChildCommand creates a new subcommand.
|
||||
func (c *Cli) NewChildCommand(name string, description ...string) *Command {
|
||||
return c.rootCommand.NewChildCommand(name, description...)
|
||||
// SetBanner sets the banner function.
|
||||
//
|
||||
// c.Cli().SetBanner(func(_ *core.Cli) string { return "My App v1.0" })
|
||||
func (cl *Cli) SetBanner(fn func(*Cli) string) {
|
||||
cl.banner = fn
|
||||
}
|
||||
|
||||
// NewChildCommandInheritFlags creates a new subcommand that inherits parent flags.
|
||||
func (c *Cli) NewChildCommandInheritFlags(name string, description ...string) *Command {
|
||||
return c.rootCommand.NewChildCommandInheritFlags(name, description...)
|
||||
}
|
||||
|
||||
// PreRun sets a function to call before running the command.
|
||||
func (c *Cli) PreRun(callback func(*Cli) error) {
|
||||
c.preRunCommand = callback
|
||||
}
|
||||
|
||||
// PostRun sets a function to call after running the command.
|
||||
func (c *Cli) PostRun(callback func(*Cli) error) {
|
||||
c.postRunCommand = callback
|
||||
}
|
||||
|
||||
// BoolFlag adds a boolean flag to the root command.
|
||||
func (c *Cli) BoolFlag(name, description string, variable *bool) *Cli {
|
||||
c.rootCommand.BoolFlag(name, description, variable)
|
||||
return c
|
||||
}
|
||||
|
||||
// StringFlag adds a string flag to the root command.
|
||||
func (c *Cli) StringFlag(name, description string, variable *string) *Cli {
|
||||
c.rootCommand.StringFlag(name, description, variable)
|
||||
return c
|
||||
}
|
||||
|
||||
// IntFlag adds an int flag to the root command.
|
||||
func (c *Cli) IntFlag(name, description string, variable *int) *Cli {
|
||||
c.rootCommand.IntFlag(name, description, variable)
|
||||
return c
|
||||
}
|
||||
|
||||
// AddFlags adds struct-tagged flags to the root command.
|
||||
func (c *Cli) AddFlags(flags any) *Cli {
|
||||
c.rootCommand.AddFlags(flags)
|
||||
return c
|
||||
}
|
||||
|
||||
// Action defines an action for the root command.
|
||||
func (c *Cli) Action(callback CliAction) *Cli {
|
||||
c.rootCommand.Action(callback)
|
||||
return c
|
||||
}
|
||||
|
||||
// LongDescription sets the long description for the root command.
|
||||
func (c *Cli) LongDescription(longdescription string) *Cli {
|
||||
c.rootCommand.LongDescription(longdescription)
|
||||
return c
|
||||
}
|
||||
|
||||
// OtherArgs returns the non-flag arguments passed to the CLI.
|
||||
func (c *Cli) OtherArgs() []string {
|
||||
return c.rootCommand.flags.Args()
|
||||
}
|
||||
|
||||
// NewChildCommandFunction creates a subcommand from a function with struct flags.
|
||||
func (c *Cli) NewChildCommandFunction(name string, description string, fn any) *Cli {
|
||||
c.rootCommand.NewChildCommandFunction(name, description, fn)
|
||||
return c
|
||||
// Banner returns the banner string.
|
||||
func (cl *Cli) Banner() string {
|
||||
if cl.banner != nil {
|
||||
return cl.banner(cl)
|
||||
}
|
||||
if cl.core != nil && cl.core.app != nil && cl.core.app.Name != "" {
|
||||
return cl.core.app.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
1464
pkg/core/command.go
1464
pkg/core/command.go
File diff suppressed because it is too large
Load diff
|
|
@ -80,7 +80,6 @@ func New(opts ...Options) *Core {
|
|||
config: &Config{ConfigOpts: &ConfigOpts{}},
|
||||
error: &ErrorPanic{},
|
||||
log: &ErrorLog{log: defaultLog},
|
||||
cli: &Cli{opts: &CliOpts{}},
|
||||
service: &Service{},
|
||||
lock: &Lock{},
|
||||
ipc: &Ipc{},
|
||||
|
|
@ -95,10 +94,8 @@ func New(opts ...Options) *Core {
|
|||
}
|
||||
}
|
||||
|
||||
// Init Cli root command from app name
|
||||
c.cli.rootCommand = NewCommand(c.app.Name)
|
||||
c.cli.rootCommand.setParentCommandPath("")
|
||||
c.cli.rootCommand.setApp(c.cli)
|
||||
// Init Cli surface with Core reference
|
||||
c.cli = &Cli{core: c}
|
||||
|
||||
return c
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,9 @@ type Core struct {
|
|||
config *Config // c.Config() — Configuration, settings, feature flags
|
||||
error *ErrorPanic // c.Error() — Panic recovery and crash reporting
|
||||
log *ErrorLog // c.Log() — Structured logging + error wrapping
|
||||
cli *Cli // c.Cli() — CLI command framework
|
||||
service *Service // c.Service("name") — Service registry and lifecycle
|
||||
cli *Cli // c.Cli() — CLI surface layer
|
||||
commands *commandRegistry // c.Command("path") — Command tree
|
||||
service *Service // c.Service("name") — Service registry and lifecycle
|
||||
lock *Lock // c.Lock("name") — Named mutexes
|
||||
ipc *Ipc // c.IPC() — Message bus for IPC
|
||||
i18n *I18n // c.I18n() — Internationalisation and locale collection
|
||||
|
|
|
|||
|
|
@ -7,70 +7,72 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- Cli ---
|
||||
// --- Cli Surface ---
|
||||
|
||||
func TestCli_Good(t *testing.T) {
|
||||
c := New()
|
||||
assert.NotNil(t, c.Cli())
|
||||
assert.NotNil(t, c.Cli().Command())
|
||||
}
|
||||
|
||||
func TestCli_Named_Good(t *testing.T) {
|
||||
func TestCli_Banner_Good(t *testing.T) {
|
||||
c := New(Options{{K: "name", V: "myapp"}})
|
||||
assert.NotNil(t, c.Cli().Command())
|
||||
assert.Equal(t, "myapp", c.Cli().Banner())
|
||||
}
|
||||
|
||||
func TestCli_NewChildCommand_Good(t *testing.T) {
|
||||
c := New(Options{{K: "name", V: "myapp"}})
|
||||
child := c.Cli().NewChildCommand("test", "a test command")
|
||||
assert.NotNil(t, child)
|
||||
}
|
||||
|
||||
func TestCli_AddCommand_Good(t *testing.T) {
|
||||
func TestCli_SetBanner_Good(t *testing.T) {
|
||||
c := New()
|
||||
cmd := NewCommand("hello", "says hello")
|
||||
c.Cli().AddCommand(cmd)
|
||||
}
|
||||
|
||||
func TestCli_Flags_Good(t *testing.T) {
|
||||
c := New()
|
||||
var name string
|
||||
var debug bool
|
||||
var port int
|
||||
c.Cli().StringFlag("name", "app name", &name)
|
||||
c.Cli().BoolFlag("debug", "enable debug", &debug)
|
||||
c.Cli().IntFlag("port", "port number", &port)
|
||||
c.Cli().SetBanner(func(_ *Cli) string { return "Custom Banner" })
|
||||
assert.Equal(t, "Custom Banner", c.Cli().Banner())
|
||||
}
|
||||
|
||||
func TestCli_Run_Good(t *testing.T) {
|
||||
c := New()
|
||||
executed := false
|
||||
c.Cli().Command().Action(func() error {
|
||||
c.Command("hello", func(_ Options) Result[any] {
|
||||
executed = true
|
||||
return nil
|
||||
return Result[any]{Value: "world", OK: true}
|
||||
})
|
||||
err := c.Cli().Run("")
|
||||
assert.NoError(t, err)
|
||||
r := c.Cli().Run("hello")
|
||||
assert.True(t, r.OK)
|
||||
assert.Equal(t, "world", r.Value)
|
||||
assert.True(t, executed)
|
||||
}
|
||||
|
||||
// --- Command ---
|
||||
|
||||
func TestCommand_New_Good(t *testing.T) {
|
||||
cmd := NewCommand("test", "a test command")
|
||||
assert.NotNil(t, cmd)
|
||||
func TestCli_Run_Nested_Good(t *testing.T) {
|
||||
c := New()
|
||||
executed := false
|
||||
c.Command("deploy/to/homelab", func(_ Options) Result[any] {
|
||||
executed = true
|
||||
return Result[any]{OK: true}
|
||||
})
|
||||
r := c.Cli().Run("deploy", "to", "homelab")
|
||||
assert.True(t, r.OK)
|
||||
assert.True(t, executed)
|
||||
}
|
||||
|
||||
func TestCommand_Child_Good(t *testing.T) {
|
||||
parent := NewCommand("root")
|
||||
child := parent.NewChildCommand("sub", "a subcommand")
|
||||
assert.NotNil(t, child)
|
||||
func TestCli_Run_WithFlags_Good(t *testing.T) {
|
||||
c := New()
|
||||
var received Options
|
||||
c.Command("serve", func(opts Options) Result[any] {
|
||||
received = opts
|
||||
return Result[any]{OK: true}
|
||||
})
|
||||
c.Cli().Run("serve", "--port=8080", "--debug")
|
||||
assert.Equal(t, "8080", received.String("port"))
|
||||
assert.True(t, received.Bool("debug"))
|
||||
}
|
||||
|
||||
func TestCommand_Flags_Good(t *testing.T) {
|
||||
cmd := NewCommand("test")
|
||||
var name string
|
||||
var debug bool
|
||||
cmd.StringFlag("name", "app name", &name)
|
||||
cmd.BoolFlag("debug", "enable debug", &debug)
|
||||
func TestCli_Run_NoCommand_Good(t *testing.T) {
|
||||
c := New()
|
||||
// No commands registered — should not panic
|
||||
r := c.Cli().Run()
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestCli_PrintHelp_Good(t *testing.T) {
|
||||
c := New(Options{{K: "name", V: "myapp"}})
|
||||
c.Command("deploy", func(_ Options) Result[any] { return Result[any]{OK: true} })
|
||||
c.Command("serve", func(_ Options) Result[any] { return Result[any]{OK: true} })
|
||||
// Should not panic
|
||||
c.Cli().PrintHelp()
|
||||
}
|
||||
|
|
|
|||
137
tests/command_test.go
Normal file
137
tests/command_test.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- Command DTO ---
|
||||
|
||||
func TestCommand_Register_Good(t *testing.T) {
|
||||
c := New()
|
||||
result := c.Command("deploy", func(_ Options) Result[any] {
|
||||
return Result[any]{Value: "deployed", OK: true}
|
||||
})
|
||||
assert.Nil(t, result) // nil = success
|
||||
}
|
||||
|
||||
func TestCommand_Get_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Command("deploy", func(_ Options) Result[any] {
|
||||
return Result[any]{OK: true}
|
||||
})
|
||||
cmd := c.Command("deploy")
|
||||
assert.NotNil(t, cmd)
|
||||
}
|
||||
|
||||
func TestCommand_Get_Bad(t *testing.T) {
|
||||
c := New()
|
||||
cmd := c.Command("nonexistent")
|
||||
assert.Nil(t, cmd)
|
||||
}
|
||||
|
||||
func TestCommand_Run_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Command("greet", func(opts Options) Result[any] {
|
||||
return Result[any]{Value: "hello " + opts.String("name"), OK: true}
|
||||
})
|
||||
cmd := c.Command("greet").(*Command)
|
||||
r := cmd.Run(Options{{K: "name", V: "world"}})
|
||||
assert.True(t, r.OK)
|
||||
assert.Equal(t, "hello world", r.Value)
|
||||
}
|
||||
|
||||
func TestCommand_Run_NoAction_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Command("empty", Options{{K: "description", V: "no action"}})
|
||||
cmd := c.Command("empty").(*Command)
|
||||
r := cmd.Run(Options{})
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
// --- Nested Commands ---
|
||||
|
||||
func TestCommand_Nested_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Command("deploy/to/homelab", func(_ Options) Result[any] {
|
||||
return Result[any]{Value: "deployed to homelab", OK: true}
|
||||
})
|
||||
|
||||
// Direct path lookup
|
||||
cmd := c.Command("deploy/to/homelab")
|
||||
assert.NotNil(t, cmd)
|
||||
|
||||
// Parent auto-created
|
||||
parent := c.Command("deploy")
|
||||
assert.NotNil(t, parent)
|
||||
|
||||
mid := c.Command("deploy/to")
|
||||
assert.NotNil(t, mid)
|
||||
}
|
||||
|
||||
func TestCommand_Paths_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Command("deploy", func(_ Options) Result[any] { return Result[any]{OK: true} })
|
||||
c.Command("serve", func(_ Options) Result[any] { return Result[any]{OK: true} })
|
||||
c.Command("deploy/to/homelab", func(_ Options) Result[any] { return Result[any]{OK: true} })
|
||||
|
||||
paths := c.Commands()
|
||||
assert.Contains(t, paths, "deploy")
|
||||
assert.Contains(t, paths, "serve")
|
||||
assert.Contains(t, paths, "deploy/to/homelab")
|
||||
assert.Contains(t, paths, "deploy/to") // auto-created parent
|
||||
}
|
||||
|
||||
// --- I18n Key Derivation ---
|
||||
|
||||
func TestCommand_I18nKey_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Command("deploy/to/homelab", func(_ Options) Result[any] { return Result[any]{OK: true} })
|
||||
cmd := c.Command("deploy/to/homelab").(*Command)
|
||||
assert.Equal(t, "cmd.deploy.to.homelab.description", cmd.I18nKey())
|
||||
}
|
||||
|
||||
func TestCommand_I18nKey_Custom_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Command("deploy", func(_ Options) Result[any] { return Result[any]{OK: true} }, Options{{K: "description", V: "custom.deploy.key"}})
|
||||
cmd := c.Command("deploy").(*Command)
|
||||
assert.Equal(t, "custom.deploy.key", cmd.I18nKey())
|
||||
}
|
||||
|
||||
func TestCommand_I18nKey_Simple_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Command("serve", func(_ Options) Result[any] { return Result[any]{OK: true} })
|
||||
cmd := c.Command("serve").(*Command)
|
||||
assert.Equal(t, "cmd.serve.description", cmd.I18nKey())
|
||||
}
|
||||
|
||||
// --- Lifecycle ---
|
||||
|
||||
func TestCommand_Lifecycle_NoImpl_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Command("serve", func(_ Options) Result[any] {
|
||||
return Result[any]{Value: "running", OK: true}
|
||||
})
|
||||
cmd := c.Command("serve").(*Command)
|
||||
|
||||
// Start falls back to Run when no lifecycle impl
|
||||
r := cmd.Start(Options{})
|
||||
assert.True(t, r.OK)
|
||||
assert.Equal(t, "running", r.Value)
|
||||
|
||||
// Stop/Restart/Reload/Signal return empty Result without lifecycle
|
||||
assert.False(t, cmd.Stop().OK)
|
||||
assert.False(t, cmd.Restart().OK)
|
||||
assert.False(t, cmd.Reload().OK)
|
||||
assert.False(t, cmd.Signal("HUP").OK)
|
||||
}
|
||||
|
||||
// --- Empty path ---
|
||||
|
||||
func TestCommand_EmptyPath_Bad(t *testing.T) {
|
||||
c := New()
|
||||
result := c.Command("", func(_ Options) Result[any] { return Result[any]{OK: true} })
|
||||
assert.NotNil(t, result) // error
|
||||
}
|
||||
1339
tests/testdata/cli_clir.go.bak
vendored
Normal file
1339
tests/testdata/cli_clir.go.bak
vendored
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue