diff --git a/app/lib/linear_cli/cli.ex b/app/lib/linear_cli/cli.ex index 010baaf..146197e 100644 --- a/app/lib/linear_cli/cli.ex +++ b/app/lib/linear_cli/cli.ex @@ -93,9 +93,37 @@ defmodule LinearCli.CLI do end defp run(fun, result, halt) do - case fun.(result) do - :ok -> :ok - {:error, error} -> handle_error(error, result.options[:debug], halt) + case reject_unknown_flags(result.unknown) do + :ok -> + case fun.(result) do + :ok -> :ok + {:error, error} -> handle_error(error, result.options[:debug], halt) + end + + {:error, error} -> + handle_error(error, result.options[:debug], halt) + end + end + + # `issue list`/`take`/`update` all set `allow_unknown_args: true` so bare + # tokens (e.g. `CRY-1`) can be captured as issue ids via `result.unknown` + # rather than a declared positional arg (Optimus has no `type: :array` + # equivalent - see their subcommand specs below). That same bucket also + # silently swallows any *unrecognized flag* (e.g. a typo, or a real flag + # this subcommand just doesn't have, like `--mine` on `issue list` - #2), + # which then gets treated as an issue id to look up instead of erroring + # clearly. Every other subcommand has `allow_unknown_args: false` (the + # default), where Optimus itself already rejects unknown args before we + # ever see a parse_result - so `result.unknown` is only ever non-empty here + # for those three subcommands, and only ever contains genuine bare ids + # once this filters out anything flag-shaped. + defp reject_unknown_flags(unknown_tokens) do + case Enum.filter(unknown_tokens, &String.starts_with?(&1, "-")) do + [] -> + :ok + + bad_flags -> + {:error, {:smells_bad, "unrecognized option(s): #{Enum.join(bad_flags, ", ")}"}} end end diff --git a/app/test/linear_cli/cli_test.exs b/app/test/linear_cli/cli_test.exs index 7586e86..0beaccc 100644 --- a/app/test/linear_cli/cli_test.exs +++ b/app/test/linear_cli/cli_test.exs @@ -131,6 +131,27 @@ defmodule LinearCli.CLITest do refute output =~ "CRY-1" end + test "issue list --mine gives a clear error instead of being treated as an issue id (#2)" do + # `--mine` isn't (and won't be - `--no-mine` already covers it, `--mine` + # is the implied default) a declared flag on `issue list`. Since this + # subcommand allows unknown args (for bare issue ids), an unrecognized + # `-`-prefixed token used to silently fall into that same bucket and get + # looked up as a literal issue id "--mine" instead of erroring - crashing + # with a raw %Ash.Error.Unknown{} dump. No Req.Test stub needed: the fix + # rejects this before any API call happens. + test_pid = self() + halt = fn code -> send(test_pid, {:halted, code}) end + + output = + capture_io(:stderr, fn -> + LinearCli.CLI.main(["issue", "list", "--mine"], halt) + end) + + assert_received {:halted, 22} + assert output =~ "unrecognized option(s): --mine" + assert output =~ "This smells bad! Bailing." + end + test "an unknown issue id halts with exit code 66" do test_pid = self() halt = fn code -> send(test_pid, {:halted, code}) end