diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-26 18:16:11 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-26 18:16:11 +0300 |
| commit | c4b872c5ec54340b1e62d7578ace400340573ce2 (patch) | |
| tree | d224707b3dd25ac68693d44f084dbe85a0edd685 /cmd/hexai-tmux-edit | |
| parent | f77d73244fa6585af0456fd374988b8b04b72646 (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.go | 25 | ||||
| -rw-r--r-- | cmd/hexai-tmux-edit/main_test.go | 65 |
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") + } +} |
