fix(cli): map invalid command args to usage
- commit
- ca15ec4e93e029b9e9da87b7a2e72ffb0953e5eb
- author
- brice · · 2026-07-24 05:40 UTC
- parent
- 2d3364c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +32 -9
M internal/app/cmd.go +3 -0
| @@ -179,6 +179,9 @@ func runCmdWithDeps(ctx context.Context, invocation cmdInvocation, deps cmdDeps) | |||
| 179 | 179 | return fmt.Errorf("decoding command reply: %w", err) | |
| 180 | 180 | } | |
| 181 | 181 | if !result.OK { | |
| 182 | + | if result.Code == ports.ErrInvalidCommandArgs { | |
| 183 | + | return &exitCoded{code: 2, err: errors.New(result.Text)} | |
| 184 | + | } | |
| 182 | 185 | return errors.New(result.Text) | |
| 183 | 186 | } | |
| 184 | 187 | if result.Output != "" { | |
| @@ -179,6 +179,9 @@ func runCmdWithDeps(ctx context.Context, invocation cmdInvocation, deps cmdDeps) | |||||
| 179 | return fmt.Errorf("decoding command reply: %w", err) | 179 | return fmt.Errorf("decoding command reply: %w", err) | ||
| 180 | } | 180 | } | ||
| 181 | if !result.OK { | 181 | if !result.OK { | ||
| 182 | + | if result.Code == ports.ErrInvalidCommandArgs { | |||
| 183 | + | return &exitCoded{code: 2, err: errors.New(result.Text)} | |||
| 184 | + | } | |||
| 182 | return errors.New(result.Text) | 185 | return errors.New(result.Text) | ||
| 183 | } | 186 | } | ||
| 184 | if result.Output != "" { | 187 | if result.Output != "" { | ||
M internal/app/cmd_test.go +29 -9
| @@ -182,15 +182,35 @@ func TestRunCmdDoesNotAutostartAndClassifiesDialFailure(t *testing.T) { | |||
| 182 | 182 | } | |
| 183 | 183 | } | |
| 184 | 184 | ||
| 185 | - | func TestRunCmdDaemonErrorHasExitOne(t *testing.T) { | |
| 186 | - | transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{Code: ports.ErrNoSuchTarget, Text: "no live sessions"})}} | |
| 187 | - | err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{ | |
| 188 | - | stdout: io.Discard, | |
| 189 | - | getenv: func(string) string { return "" }, | |
| 190 | - | dial: func(context.Context, string) (ports.Transport, error) { return transport, nil }, | |
| 191 | - | }) | |
| 192 | - | if err == nil || ExitCode(err) != 1 || !strings.Contains(err.Error(), "no live sessions") { | |
| 193 | - | t.Fatalf("error=%v code=%d", err, ExitCode(err)) | |
| 185 | + | func TestRunCmdClassifiesDaemonCommandErrors(t *testing.T) { | |
| 186 | + | tests := []struct { | |
| 187 | + | name string | |
| 188 | + | result ports.CommandResult | |
| 189 | + | wantCode int | |
| 190 | + | }{ | |
| 191 | + | { | |
| 192 | + | name: "invalid command arguments are usage errors", | |
| 193 | + | result: ports.CommandResult{Code: ports.ErrInvalidCommandArgs, Text: "usage: toast [-l level] <message>"}, | |
| 194 | + | wantCode: 2, | |
| 195 | + | }, | |
| 196 | + | { | |
| 197 | + | name: "missing runtime target is a command failure", | |
| 198 | + | result: ports.CommandResult{Code: ports.ErrNoSuchTarget, Text: "no live sessions"}, | |
| 199 | + | wantCode: 1, | |
| 200 | + | }, | |
| 201 | + | } | |
| 202 | + | for _, tt := range tests { | |
| 203 | + | t.Run(tt.name, func(t *testing.T) { | |
| 204 | + | transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(tt.result)}} | |
| 205 | + | err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{ | |
| 206 | + | stdout: io.Discard, | |
| 207 | + | getenv: func(string) string { return "" }, | |
| 208 | + | dial: func(context.Context, string) (ports.Transport, error) { return transport, nil }, | |
| 209 | + | }) | |
| 210 | + | if err == nil || ExitCode(err) != tt.wantCode || err.Error() != tt.result.Text { | |
| 211 | + | t.Fatalf("error=%v code=%d, want message %q and code %d", err, ExitCode(err), tt.result.Text, tt.wantCode) | |
| 212 | + | } | |
| 213 | + | }) | |
| 194 | 214 | } | |
| 195 | 215 | } | |
| 196 | 216 | ||
| @@ -182,15 +182,35 @@ func TestRunCmdDoesNotAutostartAndClassifiesDialFailure(t *testing.T) { | |||||
| 182 | } | 182 | } | ||
| 183 | } | 183 | } | ||
| 184 | 184 | ||||
| 185 | - | func TestRunCmdDaemonErrorHasExitOne(t *testing.T) { | 185 | + | func TestRunCmdClassifiesDaemonCommandErrors(t *testing.T) { |
| 186 | - | transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{Code: ports.ErrNoSuchTarget, Text: "no live sessions"})}} | 186 | + | tests := []struct { |
| 187 | - | err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{ | 187 | + | name string |
| 188 | - | stdout: io.Discard, | 188 | + | result ports.CommandResult |
| 189 | - | getenv: func(string) string { return "" }, | 189 | + | wantCode int |
| 190 | - | dial: func(context.Context, string) (ports.Transport, error) { return transport, nil }, | 190 | + | }{ |
| 191 | - | }) | 191 | + | { |
| 192 | - | if err == nil || ExitCode(err) != 1 || !strings.Contains(err.Error(), "no live sessions") { | 192 | + | name: "invalid command arguments are usage errors", |
| 193 | - | t.Fatalf("error=%v code=%d", err, ExitCode(err)) | 193 | + | result: ports.CommandResult{Code: ports.ErrInvalidCommandArgs, Text: "usage: toast [-l level] <message>"}, |
| 194 | + | wantCode: 2, | |||
| 195 | + | }, | |||
| 196 | + | { | |||
| 197 | + | name: "missing runtime target is a command failure", | |||
| 198 | + | result: ports.CommandResult{Code: ports.ErrNoSuchTarget, Text: "no live sessions"}, | |||
| 199 | + | wantCode: 1, | |||
| 200 | + | }, | |||
| 201 | + | } | |||
| 202 | + | for _, tt := range tests { | |||
| 203 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 204 | + | transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(tt.result)}} | |||
| 205 | + | err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{ | |||
| 206 | + | stdout: io.Discard, | |||
| 207 | + | getenv: func(string) string { return "" }, | |||
| 208 | + | dial: func(context.Context, string) (ports.Transport, error) { return transport, nil }, | |||
| 209 | + | }) | |||
| 210 | + | if err == nil || ExitCode(err) != tt.wantCode || err.Error() != tt.result.Text { | |||
| 211 | + | t.Fatalf("error=%v code=%d, want message %q and code %d", err, ExitCode(err), tt.result.Text, tt.wantCode) | |||
| 212 | + | } | |||
| 213 | + | }) | |||
| 194 | } | 214 | } | ||
| 195 | } | 215 | } | ||
| 196 | 216 | ||||