Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions app/lib/linear_cli/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 21 additions & 0 deletions app/test/linear_cli/cli_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down