From 00ba3df49c181ad924759c6580a4b96bc00519f5 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Mon, 5 Oct 2026 22:05:09 +1100 Subject: [PATCH] watchpr: reject --max-wait with --once, test exit-code mapping --- cmd/watchpr/main.go | 19 ++++++++++++++----- cmd/watchpr/main_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/cmd/watchpr/main.go b/cmd/watchpr/main.go index c1f8e02..39637a9 100644 --- a/cmd/watchpr/main.go +++ b/cmd/watchpr/main.go @@ -47,12 +47,18 @@ var errTimedOut = errors.New("max-wait elapsed with no change") func main() { // cobra prints the error itself (SilenceErrors stays off); we only need to // turn any command error into a non-zero exit. - if err := newRootCmd().Execute(); err != nil { - if errors.Is(err, errTimedOut) { - os.Exit(exitTimedOut) - } - os.Exit(1) + os.Exit(exitCode(newRootCmd().Execute())) +} + +// exitCode maps a command result to the process exit status. +func exitCode(err error) int { + switch { + case err == nil: + return 0 + case errors.Is(err, errTimedOut): + return exitTimedOut } + return 1 } // newRootCmd builds the watchpr command tree. It is separated from main so @@ -86,6 +92,9 @@ func newRootCmd() *cobra.Command { if err != nil { return err } + if once && maxWait > 0 { + return fmt.Errorf("--max-wait has no effect with --once") + } refs := make([]agent.PRRef, 0, len(args)) for _, a := range args { ref, err := agent.ParsePRRef(a) diff --git a/cmd/watchpr/main_test.go b/cmd/watchpr/main_test.go index 6331331..fff71d0 100644 --- a/cmd/watchpr/main_test.go +++ b/cmd/watchpr/main_test.go @@ -336,6 +336,36 @@ func TestExecuteBadMaxWaitErrors(t *testing.T) { } } +func TestExecuteOnceWithMaxWaitErrors(t *testing.T) { + cmd := newRootCmd() + cmd.SetArgs([]string{"--once", "--max-wait", "5m", "unkin/repo#1"}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + err := cmd.Execute() + if err == nil || !strings.Contains(err.Error(), "--once") { + t.Fatalf("Execute() = %v, want a --once/--max-wait conflict error", err) + } + if got := exitCode(err); got != 1 { + t.Fatalf("exitCode = %d, want 1", got) + } +} + +func TestExitCode(t *testing.T) { + for _, tc := range []struct { + err error + want int + }{ + {nil, 0}, + {errTimedOut, exitTimedOut}, + {fmt.Errorf("wrapped: %w", errTimedOut), exitTimedOut}, + {errors.New("boom"), 1}, + } { + if got := exitCode(tc.err); got != tc.want { + t.Errorf("exitCode(%v) = %d, want %d", tc.err, got, tc.want) + } + } +} + // Ticks pass through until the deadline fires; then the channel closes without // waiting for another tick. func TestUntilDeadline(t *testing.T) {