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/ask | |
| 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/ask')
| -rw-r--r-- | cmd/ask/main.go | 31 | ||||
| -rw-r--r-- | cmd/ask/main_test.go | 69 |
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()) + } +} |
