summaryrefslogtreecommitdiff
path: root/cmd/hexai-tmux-edit
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-26 18:16:11 +0300
committerPaul Buetow <paul@buetow.org>2026-04-26 18:16:11 +0300
commitc4b872c5ec54340b1e62d7578ace400340573ce2 (patch)
treed224707b3dd25ac68693d44f084dbe85a0edd685 /cmd/hexai-tmux-edit
parentf77d73244fa6585af0456fd374988b8b04b72646 (diff)
test: bring every package above the 80% coverage target
Per-package coverage was below the AGENTS.md target in six packages: cmd/ask 0.0% -> 83.3% cmd/hexai-tmux-edit 10.0% -> 93.3% cmd/hexai-tmux-action 27.8% -> 95.7% cmd/hexai-mcp-server 41.9% -> 88.2% internal/taskproxy 61.8% -> 98.2% internal/filelock 77.3% -> 100.0% The four cmd packages each had a main() that mixed flag parsing, struct construction, and runtime delegation, so nothing called from a test hit those statements. Each main() is now a one-line wrapper around a testable runMain(args, stdin, stdout, stderr) int that uses flag.NewFlagSet (instead of the global flag.Parse) so tests can drive it repeatedly. The deprecation banner in hexai-mcp-server is now a package-level constant, kept identical, so tests can assert on it directly without redirecting os.Stderr. The internal packages got new tests for paths that were previously unreachable: filelock's retry-then-success and non-EWOULDBLOCK error branches, and taskproxy's NewRunner / findTaskBinary / detectRepoRoot / runTaskCommand helpers (the ones that shell out to git and task). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'cmd/hexai-tmux-edit')
-rw-r--r--cmd/hexai-tmux-edit/main.go25
-rw-r--r--cmd/hexai-tmux-edit/main_test.go65
2 files changed, 83 insertions, 7 deletions
diff --git a/cmd/hexai-tmux-edit/main.go b/cmd/hexai-tmux-edit/main.go
index 6d0e75e..6177008 100644
--- a/cmd/hexai-tmux-edit/main.go
+++ b/cmd/hexai-tmux-edit/main.go
@@ -14,6 +14,7 @@ package main
import (
"flag"
"fmt"
+ "io"
"os"
"strings"
@@ -24,18 +25,28 @@ import (
// runTmuxEdit is the seam for testing: override in tests to avoid real tmux.
var runTmuxEdit = tmuxedit.Run
-func main() {
+func main() { os.Exit(runMain(os.Args[1:], os.Stderr)) }
+
+// runMain parses flags from args and runs the tmux edit popup. It returns
+// the process exit code; flag errors return 2 (matching stdlib convention),
+// runtime failures return 1.
+func runMain(args []string, stderr io.Writer) int {
defaultPath := appconfig.DefaultConfigPath()
- configPath := flag.String("config", "", fmt.Sprintf("path to config file (default: %s)", defaultPath))
- agent := flag.String("agent", "", "AI agent name (auto-detected if omitted)")
- pane := flag.String("pane", "", "tmux target pane ID (e.g. %%5)")
- flag.Parse()
+ fs := flag.NewFlagSet("hexai-tmux-edit", flag.ContinueOnError)
+ fs.SetOutput(stderr)
+ configPath := fs.String("config", "", fmt.Sprintf("path to config file (default: %s)", defaultPath))
+ agent := fs.String("agent", "", "AI agent name (auto-detected if omitted)")
+ pane := fs.String("pane", "", "tmux target pane ID (e.g. %5)")
+ if err := fs.Parse(args); err != nil {
+ return 2
+ }
opts := buildOptions(*configPath, *agent, *pane)
if err := runTmuxEdit(opts); err != nil {
- fmt.Fprintln(os.Stderr, err)
- os.Exit(1)
+ fmt.Fprintln(stderr, err)
+ return 1
}
+ return 0
}
// buildOptions constructs tmuxedit.Options from the parsed flag values,
diff --git a/cmd/hexai-tmux-edit/main_test.go b/cmd/hexai-tmux-edit/main_test.go
index fc2364c..6881556 100644
--- a/cmd/hexai-tmux-edit/main_test.go
+++ b/cmd/hexai-tmux-edit/main_test.go
@@ -1,7 +1,9 @@
package main
import (
+ "bytes"
"errors"
+ "strings"
"testing"
"codeberg.org/snonux/hexai/internal/tmuxedit"
@@ -57,3 +59,66 @@ func TestRunTmuxEdit_Error(t *testing.T) {
t.Fatalf("expected error, got: %v", err)
}
}
+
+// runMain happy path: flags parse, runTmuxEdit returns nil, exit code 0.
+// We capture the resolved Options to confirm flags map onto fields correctly.
+func TestRunMain_FlagsForwardedToTmuxedit(t *testing.T) {
+ old := runTmuxEdit
+ t.Cleanup(func() { runTmuxEdit = old })
+
+ var got tmuxedit.Options
+ runTmuxEdit = func(opts tmuxedit.Options) error {
+ got = opts
+ return nil
+ }
+
+ var stderr bytes.Buffer
+ code := runMain([]string{"-config", " /tmp/cfg.toml ", "-agent", "claude", "-pane", "%9"}, &stderr)
+ if code != 0 {
+ t.Fatalf("runMain code = %d, want 0", code)
+ }
+ if got.ConfigPath != "/tmp/cfg.toml" || got.Agent != "claude" || got.Pane != "%9" {
+ t.Fatalf("unexpected opts: %+v", got)
+ }
+ if stderr.Len() != 0 {
+ t.Fatalf("stderr should be empty on success, got %q", stderr.String())
+ }
+}
+
+// runMain reports tmuxedit.Run failures by writing to stderr and returning 1
+// — the production exit code that the shipped binary uses.
+func TestRunMain_RunErrorReturnsOne(t *testing.T) {
+ old := runTmuxEdit
+ t.Cleanup(func() { runTmuxEdit = old })
+ runTmuxEdit = func(tmuxedit.Options) error { return errors.New("boom") }
+
+ var stderr bytes.Buffer
+ code := runMain(nil, &stderr)
+ if code != 1 {
+ t.Fatalf("runMain code = %d, want 1", code)
+ }
+ if !strings.Contains(stderr.String(), "boom") {
+ t.Fatalf("stderr missing error: %q", stderr.String())
+ }
+}
+
+// Unknown flags must yield exit 2 (the convention used by stdlib `flag` when
+// ExitOnError aborts) without ever invoking runTmuxEdit.
+func TestRunMain_BadFlagReturnsTwo(t *testing.T) {
+ old := runTmuxEdit
+ t.Cleanup(func() { runTmuxEdit = old })
+ called := false
+ runTmuxEdit = func(tmuxedit.Options) error {
+ called = true
+ return nil
+ }
+
+ var stderr bytes.Buffer
+ code := runMain([]string{"--no-such-flag"}, &stderr)
+ if code != 2 {
+ t.Fatalf("runMain code = %d, want 2", code)
+ }
+ if called {
+ t.Fatal("runTmuxEdit must not be called on flag-parse failure")
+ }
+}