diff options
Diffstat (limited to 'cmd/hexai-tmux-action')
| -rw-r--r-- | cmd/hexai-tmux-action/main.go | 36 | ||||
| -rw-r--r-- | cmd/hexai-tmux-action/main_test.go | 77 |
2 files changed, 101 insertions, 12 deletions
diff --git a/cmd/hexai-tmux-action/main.go b/cmd/hexai-tmux-action/main.go index a0f240b..e2f50eb 100644 --- a/cmd/hexai-tmux-action/main.go +++ b/cmd/hexai-tmux-action/main.go @@ -16,26 +16,38 @@ import ( // the real tmux action. var runCommand = hexaiaction.RunCommand -func main() { - infile := flag.String("infile", "", "Read input from this file instead of stdin") - outfile := flag.String("outfile", "", "Write output to this file instead of stdout") - uiChild := flag.Bool("ui-child", false, "INTERNAL: run interactive UI and write to -outfile atomically") +func main() { os.Exit(runMain(os.Args[1:], os.Stdin, os.Stdout, os.Stderr)) } + +// runMain parses command-line flags from args, builds actionOptions, and +// delegates to run. It returns the process exit code: 2 for flag-parse +// errors (matching stdlib `flag.ExitOnError`), 1 for runtime failures, 0 on +// success. Splitting the body out of main keeps it testable without +// touching package-level flag state. +func runMain(args []string, stdin io.Reader, stdout, stderr io.Writer) int { + fs := flag.NewFlagSet("hexai-tmux-action", flag.ContinueOnError) + fs.SetOutput(stderr) + infile := fs.String("infile", "", "Read input from this file instead of stdin") + outfile := fs.String("outfile", "", "Write output to this file instead of stdout") + uiChild := fs.Bool("ui-child", false, "INTERNAL: run interactive UI and write to -outfile atomically") defaultPath := appconfig.DefaultConfigPath() - configPath := flag.String("config", "", fmt.Sprintf("path to config file (default: %s)", defaultPath)) - tmuxTarget := flag.String("tmux-target", "", "tmux popup target pane (advanced)") - tmuxPopupWidth := flag.String("tmux-popup-width", "60%", "tmux popup width, e.g. 60% or 120") - tmuxPopupHeight := flag.String("tmux-popup-height", "50%", "tmux popup height, e.g. 50% or 30") - flag.Parse() + configPath := fs.String("config", "", fmt.Sprintf("path to config file (default: %s)", defaultPath)) + tmuxTarget := fs.String("tmux-target", "", "tmux popup target pane (advanced)") + tmuxPopupWidth := fs.String("tmux-popup-width", "60%", "tmux popup width, e.g. 60% or 120") + tmuxPopupHeight := fs.String("tmux-popup-height", "50%", "tmux popup height, e.g. 50% or 30") + if err := fs.Parse(args); err != nil { + return 2 + } opts := actionOptions{ infile: *infile, outfile: *outfile, uiChild: *uiChild, configPath: *configPath, tmuxTarget: *tmuxTarget, tmuxPopupWidth: *tmuxPopupWidth, tmuxPopupHeight: *tmuxPopupHeight, } - if err := run(opts, os.Stdin, os.Stdout, os.Stderr); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) + if err := run(opts, stdin, stdout, stderr); err != nil { + fmt.Fprintln(stderr, err) + return 1 } + return 0 } // actionOptions holds the parsed command-line flags for hexai-tmux-action. diff --git a/cmd/hexai-tmux-action/main_test.go b/cmd/hexai-tmux-action/main_test.go index c2cc95f..e1c02e1 100644 --- a/cmd/hexai-tmux-action/main_test.go +++ b/cmd/hexai-tmux-action/main_test.go @@ -1,9 +1,11 @@ package main import ( + "bytes" "context" "errors" "io" + "strings" "testing" "codeberg.org/snonux/hexai/internal/hexaiaction" @@ -61,3 +63,78 @@ func TestRun_Error(t *testing.T) { t.Fatalf("expected error, got: %v", err) } } + +// runMain happy path: every flag is forwarded into hexaiaction.Options and +// the stub returns 0. The captured Options confirm the field-by-field +// mapping that main relies on. +func TestRunMain_FlagsForwardedToHexaiaction(t *testing.T) { + old := runCommand + t.Cleanup(func() { runCommand = old }) + + var got hexaiaction.Options + runCommand = func(_ context.Context, opts hexaiaction.Options, _ io.Reader, _, _ io.Writer) error { + got = opts + return nil + } + + args := []string{ + "-infile", "in.txt", + "-outfile", "out.txt", + "-tmux-target", "%2", + "-tmux-popup-width", "70%", + "-tmux-popup-height", "40%", + "-ui-child", + } + var stderr bytes.Buffer + code := runMain(args, nil, &bytes.Buffer{}, &stderr) + if code != 0 { + t.Fatalf("runMain code = %d, want 0; stderr=%q", code, stderr.String()) + } + if got.Infile != "in.txt" || got.Outfile != "out.txt" { + t.Fatalf("infile/outfile mismatch: %+v", got) + } + if got.TmuxTarget != "%2" || got.TmuxPopupWidth != "70%" || got.TmuxPopupHeight != "40%" { + t.Fatalf("tmux flags mismatch: %+v", got) + } + if !got.UIChild { + t.Fatal("expected UIChild=true") + } +} + +// On runCommand failure, runMain returns 1 (the production exit code) and +// writes the error message to stderr so users see what went wrong. +func TestRunMain_RuntimeErrorReturnsOne(t *testing.T) { + old := runCommand + t.Cleanup(func() { runCommand = old }) + runCommand = func(context.Context, hexaiaction.Options, io.Reader, io.Writer, io.Writer) error { + return errors.New("action exploded") + } + + var stderr bytes.Buffer + code := runMain(nil, nil, &bytes.Buffer{}, &stderr) + if code != 1 { + t.Fatalf("runMain code = %d, want 1", code) + } + if !strings.Contains(stderr.String(), "action exploded") { + t.Fatalf("stderr missing error: %q", stderr.String()) + } +} + +// Bad flag must yield exit 2 without ever invoking runCommand. +func TestRunMain_BadFlagReturnsTwo(t *testing.T) { + old := runCommand + t.Cleanup(func() { runCommand = old }) + called := false + runCommand = func(context.Context, hexaiaction.Options, io.Reader, io.Writer, io.Writer) error { + called = true + return nil + } + var stderr bytes.Buffer + code := runMain([]string{"--bogus"}, nil, &bytes.Buffer{}, &stderr) + if code != 2 { + t.Fatalf("runMain code = %d, want 2", code) + } + if called { + t.Fatal("runCommand must not be called on flag-parse failure") + } +} |
