summaryrefslogtreecommitdiff
path: root/cmd/ask
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/ask
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/ask')
-rw-r--r--cmd/ask/main.go31
-rw-r--r--cmd/ask/main_test.go69
2 files changed, 91 insertions, 9 deletions
diff --git a/cmd/ask/main.go b/cmd/ask/main.go
index afab992..fbd5bb0 100644
--- a/cmd/ask/main.go
+++ b/cmd/ask/main.go
@@ -3,20 +3,33 @@ package main
import (
"context"
"fmt"
+ "io"
"os"
"codeberg.org/snonux/hexai/internal/askcli"
)
-func main() {
- d := askcli.NewDispatcher(nil)
- code, err := d.Dispatch(context.Background(), os.Args[1:], os.Stdin, os.Stdout, os.Stderr)
+// dispatcher is the minimal interface runMain depends on; it matches
+// (*askcli.Dispatcher).Dispatch so a real dispatcher satisfies it directly.
+type dispatcher interface {
+ Dispatch(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error)
+}
+
+// dispatcherFactory is a test seam: override to inject a fake dispatcher so
+// runMain can be exercised without a real `task` binary on PATH.
+var dispatcherFactory = func() dispatcher {
+ return askcli.NewDispatcher(nil)
+}
+
+func main() { os.Exit(runMain(os.Args[1:], os.Stdin, os.Stdout, os.Stderr)) }
+
+// runMain dispatches the command and returns the process exit code; errors
+// are printed to stderr. The dispatcher's exit code is returned regardless
+// of err so callers see Taskwarrior's own exit code on failure paths.
+func runMain(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+ code, err := dispatcherFactory().Dispatch(context.Background(), args, stdin, stdout, stderr)
if err != nil {
- // Print the internal error so callers get a useful diagnostic message.
- fmt.Fprintln(os.Stderr, err)
- os.Exit(code)
- }
- if code != 0 {
- os.Exit(code)
+ fmt.Fprintln(stderr, err)
}
+ return code
}
diff --git a/cmd/ask/main_test.go b/cmd/ask/main_test.go
index db6b436..643dde4 100644
--- a/cmd/ask/main_test.go
+++ b/cmd/ask/main_test.go
@@ -3,7 +3,9 @@ package main
import (
"bytes"
"context"
+ "errors"
"io"
+ "strings"
"testing"
"codeberg.org/snonux/hexai/internal/askcli"
@@ -49,3 +51,70 @@ type spyRunner struct {
func (s *spyRunner) Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
return s.runFn(ctx, args, stdin, stdout, stderr)
}
+
+// fakeDispatcher captures Dispatch arguments and returns canned values; used
+// to exercise runMain without a real Taskwarrior binary on PATH.
+type fakeDispatcher struct {
+ gotArgs []string
+ code int
+ err error
+}
+
+func (f *fakeDispatcher) Dispatch(_ context.Context, args []string, _ io.Reader, _, _ io.Writer) (int, error) {
+ f.gotArgs = append([]string(nil), args...)
+ return f.code, f.err
+}
+
+// Driving runMain through a fake dispatcher proves the wiring (args
+// forwarded, exit code returned) without touching the real ask CLI.
+func TestRunMain_DelegatesAndReturnsCode(t *testing.T) {
+ old := dispatcherFactory
+ t.Cleanup(func() { dispatcherFactory = old })
+
+ fake := &fakeDispatcher{code: 0}
+ dispatcherFactory = func() dispatcher { return fake }
+
+ var stdout, stderr bytes.Buffer
+ got := runMain([]string{"list", "limit:1"}, nil, &stdout, &stderr)
+ if got != 0 {
+ t.Fatalf("runMain code = %d, want 0", got)
+ }
+ if len(fake.gotArgs) != 2 || fake.gotArgs[0] != "list" || fake.gotArgs[1] != "limit:1" {
+ t.Fatalf("Dispatch args = %v", fake.gotArgs)
+ }
+ if stderr.Len() != 0 {
+ t.Fatalf("stderr should be empty on success, got %q", stderr.String())
+ }
+}
+
+// The default dispatcherFactory must return a working real dispatcher (this
+// is the path main() uses in production); fakes used elsewhere don't cover
+// it, so verify it explicitly.
+func TestDispatcherFactory_DefaultReturnsRealDispatcher(t *testing.T) {
+ d := dispatcherFactory()
+ if d == nil {
+ t.Fatal("default dispatcherFactory returned nil")
+ }
+ if _, ok := d.(*askcli.Dispatcher); !ok {
+ t.Fatalf("default dispatcherFactory returned %T, want *askcli.Dispatcher", d)
+ }
+}
+
+// On a dispatcher error, runMain must print the error to stderr AND surface
+// the dispatcher's exit code so the shell sees Taskwarrior's own status.
+func TestRunMain_PrintsErrorAndPropagatesExitCode(t *testing.T) {
+ old := dispatcherFactory
+ t.Cleanup(func() { dispatcherFactory = old })
+
+ fake := &fakeDispatcher{code: 7, err: errors.New("dispatch boom")}
+ dispatcherFactory = func() dispatcher { return fake }
+
+ var stdout, stderr bytes.Buffer
+ got := runMain(nil, nil, &stdout, &stderr)
+ if got != 7 {
+ t.Fatalf("runMain code = %d, want 7", got)
+ }
+ if !strings.Contains(stderr.String(), "dispatch boom") {
+ t.Fatalf("stderr missing error text: %q", stderr.String())
+ }
+}