summaryrefslogtreecommitdiff
path: root/cmd/hexai-tmux-action
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-18 07:45:37 +0300
committerPaul Buetow <paul@buetow.org>2026-06-18 07:45:37 +0300
commit4ffb22e7f69f1c9c79b095d4e60bad3d97aac55b (patch)
tree2dc708cbb95975d34084eb5afd376871caa13e27 /cmd/hexai-tmux-action
parentece7dcfd232b780f5650326c8ac2379ca70387d4 (diff)
ik0 replace test seams with dependency injection
Diffstat (limited to 'cmd/hexai-tmux-action')
-rw-r--r--cmd/hexai-tmux-action/main.go32
-rw-r--r--cmd/hexai-tmux-action/main_test.go52
2 files changed, 40 insertions, 44 deletions
diff --git a/cmd/hexai-tmux-action/main.go b/cmd/hexai-tmux-action/main.go
index e2f50eb..8830aa8 100644
--- a/cmd/hexai-tmux-action/main.go
+++ b/cmd/hexai-tmux-action/main.go
@@ -12,18 +12,20 @@ import (
"codeberg.org/snonux/hexai/internal/hexaiaction"
)
-// runCommand is the seam for testing: override in tests to avoid launching
-// the real tmux action.
-var runCommand = hexaiaction.RunCommand
+// actionRunner is the dependency that performs the tmux action. Production
+// code uses hexaiaction.RunCommand; tests inject a stub to avoid launching the
+// real tmux popup.
+type actionRunner func(context.Context, hexaiaction.Options, io.Reader, io.Writer, io.Writer) error
-func main() { os.Exit(runMain(os.Args[1:], os.Stdin, os.Stdout, os.Stderr)) }
+func main() { os.Exit(newApp().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 {
+// touching package-level flag state. It is a method on app so tests can inject
+// a stub runCommand.
+func (a *app) 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")
@@ -43,7 +45,7 @@ func runMain(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
uiChild: *uiChild, configPath: *configPath,
tmuxTarget: *tmuxTarget, tmuxPopupWidth: *tmuxPopupWidth, tmuxPopupHeight: *tmuxPopupHeight,
}
- if err := run(opts, stdin, stdout, stderr); err != nil {
+ if err := a.run(opts, stdin, stdout, stderr); err != nil {
fmt.Fprintln(stderr, err)
return 1
}
@@ -61,8 +63,18 @@ type actionOptions struct {
tmuxPopupHeight string
}
-// run builds the hexaiaction.Options and context, then delegates to runCommand.
-func run(opts actionOptions, stdin io.Reader, stdout, stderr io.Writer) error {
+// app wires the injected dependencies for the command. runCommand defaults to
+// hexaiaction.RunCommand in production and is replaced by tests.
+type app struct {
+ runCommand actionRunner
+}
+
+// newApp returns an app with the production action runner installed.
+func newApp() *app { return &app{runCommand: hexaiaction.RunCommand} }
+
+// run builds the hexaiaction.Options and context, then delegates to the
+// injected runCommand dependency.
+func (a *app) run(opts actionOptions, stdin io.Reader, stdout, stderr io.Writer) error {
haOpts := hexaiaction.Options{
Infile: opts.infile, Outfile: opts.outfile,
UIChild: opts.uiChild, TmuxTarget: opts.tmuxTarget,
@@ -72,5 +84,5 @@ func run(opts actionOptions, stdin io.Reader, stdout, stderr io.Writer) error {
if path := strings.TrimSpace(opts.configPath); path != "" {
ctx = hexaiaction.WithConfigPath(ctx, path)
}
- return runCommand(ctx, haOpts, stdin, stdout, stderr)
+ return a.runCommand(ctx, haOpts, stdin, stdout, stderr)
}
diff --git a/cmd/hexai-tmux-action/main_test.go b/cmd/hexai-tmux-action/main_test.go
index e1c02e1..98f0c7c 100644
--- a/cmd/hexai-tmux-action/main_test.go
+++ b/cmd/hexai-tmux-action/main_test.go
@@ -12,20 +12,17 @@ import (
)
func TestRun_DelegatesToRunCommand(t *testing.T) {
- old := runCommand
- t.Cleanup(func() { runCommand = old })
-
var gotOpts hexaiaction.Options
- runCommand = func(_ context.Context, opts hexaiaction.Options, _ io.Reader, _, _ io.Writer) error {
+ a := &app{runCommand: func(_ context.Context, opts hexaiaction.Options, _ io.Reader, _, _ io.Writer) error {
gotOpts = opts
return nil
- }
+ }}
opts := actionOptions{
infile: "in.txt", outfile: "out.txt",
tmuxPopupWidth: "90%", tmuxPopupHeight: "70%",
}
- if err := run(opts, nil, nil, nil); err != nil {
+ if err := a.run(opts, nil, nil, nil); err != nil {
t.Fatalf("run: %v", err)
}
if gotOpts.Infile != "in.txt" || gotOpts.Outfile != "out.txt" {
@@ -37,29 +34,23 @@ func TestRun_DelegatesToRunCommand(t *testing.T) {
}
func TestRun_WithConfigPath(t *testing.T) {
- old := runCommand
- t.Cleanup(func() { runCommand = old })
-
- runCommand = func(_ context.Context, _ hexaiaction.Options, _ io.Reader, _, _ io.Writer) error {
+ a := &app{runCommand: func(_ context.Context, _ hexaiaction.Options, _ io.Reader, _, _ io.Writer) error {
return nil
- }
+ }}
opts := actionOptions{configPath: " /tmp/test.toml "}
- if err := run(opts, nil, nil, nil); err != nil {
+ if err := a.run(opts, nil, nil, nil); err != nil {
t.Fatalf("run: %v", err)
}
}
func TestRun_Error(t *testing.T) {
- old := runCommand
- t.Cleanup(func() { runCommand = old })
-
wantErr := errors.New("action failed")
- runCommand = func(_ context.Context, _ hexaiaction.Options, _ io.Reader, _, _ io.Writer) error {
+ a := &app{runCommand: func(_ context.Context, _ hexaiaction.Options, _ io.Reader, _, _ io.Writer) error {
return wantErr
- }
+ }}
- if err := run(actionOptions{}, nil, nil, nil); !errors.Is(err, wantErr) {
+ if err := a.run(actionOptions{}, nil, nil, nil); !errors.Is(err, wantErr) {
t.Fatalf("expected error, got: %v", err)
}
}
@@ -68,14 +59,11 @@ func TestRun_Error(t *testing.T) {
// 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 {
+ a := &app{runCommand: func(_ context.Context, opts hexaiaction.Options, _ io.Reader, _, _ io.Writer) error {
got = opts
return nil
- }
+ }}
args := []string{
"-infile", "in.txt",
@@ -86,7 +74,7 @@ func TestRunMain_FlagsForwardedToHexaiaction(t *testing.T) {
"-ui-child",
}
var stderr bytes.Buffer
- code := runMain(args, nil, &bytes.Buffer{}, &stderr)
+ code := a.runMain(args, nil, &bytes.Buffer{}, &stderr)
if code != 0 {
t.Fatalf("runMain code = %d, want 0; stderr=%q", code, stderr.String())
}
@@ -104,14 +92,12 @@ func TestRunMain_FlagsForwardedToHexaiaction(t *testing.T) {
// 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 {
+ a := &app{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)
+ code := a.runMain(nil, nil, &bytes.Buffer{}, &stderr)
if code != 1 {
t.Fatalf("runMain code = %d, want 1", code)
}
@@ -122,15 +108,13 @@ func TestRunMain_RuntimeErrorReturnsOne(t *testing.T) {
// 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 {
+ a := &app{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)
+ code := a.runMain([]string{"--bogus"}, nil, &bytes.Buffer{}, &stderr)
if code != 2 {
t.Fatalf("runMain code = %d, want 2", code)
}