From b8a7b7e3f3fb97e9fb2388d810f189ff3804889c Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Sun, 16 Aug 2026 10:46:30 -0400 Subject: [PATCH 1/4] feat(issue): add lc issue status command Adds `lc issue status ISSUE_ID` to change an issue's workflow state. - --status/-s sets the state by name (case-insensitive, prefix match); prompts interactively when omitted - --comment/-m adds a comment alongside the status change - "s" subcommand alias mirrors the other single-letter issue aliases - Resolves ISSUE_ID via the existing expand_issue_id/1 convention - Errors clearly on unknown or ambiguous status names (exit 22) - JSON output mode emits the updated issue struct; text mode adds a confirmation line - Backed by a new :set_status Ash action on LinearCli.Linear.Issue (LinearCli.Linear.Issue.Update.SetStatus manual update module) and a set_issue_status/2 domain code interface - 9 new tests covering: exact match, -s short flag, prefix match, unknown name, ambiguous name, --comment, interactive selection, --output json, and alias routing Co-Authored-By: Claude Sonnet 4.6 --- app/lib/linear_cli/cli.ex | 21 ++ app/lib/linear_cli/cli/commands.ex | 67 ++++ app/lib/linear_cli/linear.ex | 1 + app/lib/linear_cli/linear/issue.ex | 18 + .../linear_cli/cli/issue_commands_test.exs | 331 ++++++++++++++++++ 5 files changed, 438 insertions(+) diff --git a/app/lib/linear_cli/cli.ex b/app/lib/linear_cli/cli.ex index 3960e3f..a717b10 100644 --- a/app/lib/linear_cli/cli.ex +++ b/app/lib/linear_cli/cli.ex @@ -100,6 +100,7 @@ defmodule LinearCli.CLI do "dev" => "develop", "l" => "list", "ls" => "list", + "s" => "status", "u" => "update", "pull-request" => "pr" }, @@ -203,6 +204,7 @@ defmodule LinearCli.CLI do defp dispatch([:issue, :pr], result, halt), do: run(&Commands.issue_pr/1, result, halt) defp dispatch([:issue, :take], result, halt), do: run(&Commands.issue_take/1, result, halt) + defp dispatch([:issue, :status], result, halt), do: run(&Commands.issue_status/1, result, halt) defp dispatch([:issue, :update], result, halt), do: run(&Commands.issue_update/1, result, halt) # A valid subcommand path that stops short of a leaf (e.g. `lc project` @@ -594,6 +596,25 @@ defmodule LinearCli.CLI do description: [long: "--description", help: "The description of the PR"] ] ], + status: [ + name: "status", + about: "Change the workflow state of an issue", + args: [ + issue_id: [value_name: "ISSUE_ID", help: "The Issue (i.e. CRY-1)", required: true] + ], + options: [ + status: [ + short: "-s", + long: "--status", + help: "Workflow state name to set (prompts if omitted)" + ], + comment: [ + short: "-m", + long: "--comment", + help: "Comment to add alongside the status change" + ] + ] + ], take: [ name: "take", about: "Assign one or more issues to yourself", diff --git a/app/lib/linear_cli/cli/commands.ex b/app/lib/linear_cli/cli/commands.ex index 7aa214b..5f6623c 100644 --- a/app/lib/linear_cli/cli/commands.ex +++ b/app/lib/linear_cli/cli/commands.ex @@ -475,4 +475,71 @@ defmodule LinearCli.CLI.Commands do defp validate_issue_ids([]), do: {:error, {:smells_bad, "No issue IDs provided!"}} defp validate_issue_ids(_issue_ids), do: :ok + + @doc """ + New command. Changes the workflow state of an issue. + + With `--status`/`-s`, matches the given name against the issue's team's + workflow states (case-insensitive exact, then unique prefix). Without it, + prompts interactively via `LinearCli.CLI.Prompt.select/2`. + + With `--comment`/`-m`, adds a comment to the issue before transitioning. + """ + @spec issue_status(Optimus.ParseResult.t()) :: :ok | {:error, term()} + def issue_status(%{args: %{issue_id: issue_id}, options: options}) do + expanded_id = IssueHelpers.expand_issue_id(issue_id) + + with {:ok, [issue]} <- Linear.issues(%{ids: [expanded_id]}), + {:ok, states} <- Linear.workflow_states_by_team(issue.team.id), + {:ok, target_state} <- resolve_target_state(states, options.status), + :ok <- maybe_add_status_comment(issue, options.comment), + {:ok, updated} <- Linear.set_issue_status(issue, target_state.id) do + Display.show(updated, %{output: options.output}) + if options.output != "json", do: Prompt.ok("#{updated.identifier} status set to #{target_state.name}") + :ok + end + end + + defp resolve_target_state(states, nil) do + choices = Enum.sort_by(states, & &1.position) |> Enum.map(&{&1.name, &1}) + {:ok, Prompt.select("Choose a status", choices)} + end + + defp resolve_target_state(states, name) do + lower = String.downcase(name) + + case Enum.filter(states, &(String.downcase(&1.name) == lower)) do + [state] -> + {:ok, state} + + [] -> + matches = Enum.filter(states, &String.starts_with?(String.downcase(&1.name), lower)) + + case matches do + [state] -> + {:ok, state} + + [] -> + available = Enum.map_join(states, ", ", & &1.name) + {:error, {:smells_bad, "Unknown status #{inspect(name)}. Available: #{available}"}} + + many -> + ambiguous = Enum.map_join(many, ", ", & &1.name) + {:error, {:smells_bad, "Ambiguous status #{inspect(name)}: matches #{ambiguous}"}} + end + + many -> + ambiguous = Enum.map_join(many, ", ", & &1.name) + {:error, {:smells_bad, "Ambiguous status #{inspect(name)}: matches #{ambiguous}"}} + end + end + + defp maybe_add_status_comment(_issue, nil), do: :ok + + defp maybe_add_status_comment(issue, comment) do + case IssueHelpers.issue_comment(issue, comment) do + {:ok, _} -> :ok + {:error, reason} -> {:error, reason} + end + end end diff --git a/app/lib/linear_cli/linear.ex b/app/lib/linear_cli/linear.ex index 87883c8..ded07c0 100644 --- a/app/lib/linear_cli/linear.ex +++ b/app/lib/linear_cli/linear.ex @@ -35,6 +35,7 @@ defmodule LinearCli.Linear do define :assign_issue, action: :assign, args: [:assignee_id] define :attach_issue_to_project, action: :attach_to_project, args: [:project_id] define :close_issue, action: :close, args: [:state_id] + define :set_issue_status, action: :set_status, args: [:state_id] end resource LinearCli.Linear.Label do diff --git a/app/lib/linear_cli/linear/issue.ex b/app/lib/linear_cli/linear/issue.ex index 63df9c6..269aa3f 100644 --- a/app/lib/linear_cli/linear/issue.ex +++ b/app/lib/linear_cli/linear/issue.ex @@ -48,6 +48,11 @@ defmodule LinearCli.Linear.Issue do argument :trash, :boolean, default: false manual LinearCli.Linear.Issue.Update.Close end + + update :set_status do + argument :state_id, :string, allow_nil?: false + manual LinearCli.Linear.Issue.Update.SetStatus + end end attributes do @@ -360,3 +365,16 @@ defmodule LinearCli.Linear.Issue.Update.Close do Issue.Update.run(changeset.data.identifier, input) end end + +defmodule LinearCli.Linear.Issue.Update.SetStatus do + @moduledoc false + use Ash.Resource.ManualUpdate + + alias LinearCli.Linear.Issue + + def update(changeset, _opts, _context) do + Issue.Update.run(changeset.data.identifier, %{ + "stateId" => changeset.arguments.state_id + }) + end +end diff --git a/app/test/linear_cli/cli/issue_commands_test.exs b/app/test/linear_cli/cli/issue_commands_test.exs index dfa6892..a6d31a1 100644 --- a/app/test/linear_cli/cli/issue_commands_test.exs +++ b/app/test/linear_cli/cli/issue_commands_test.exs @@ -536,6 +536,337 @@ defmodule LinearCli.CLI.IssueCommandsTest do end end + describe "issue status" do + defp state_map(id, name, position, type) do + %{"id" => id, "name" => name, "position" => position, "type" => type, "description" => nil} + end + + defp issue_with_state(state_id, state_name) do + issue_map(%{"state" => %{"id" => state_id, "name" => state_name, "type" => "started"}}) + end + + defp states_response do + workflow_states([ + state_map("s1", "Triage", 0.0, "triage"), + state_map("s2", "In Progress", 1.0, "started"), + state_map("s3", "Done", 2.0, "completed") + ]) + end + + test "--status sets the workflow state by exact name (case-insensitive)" do + test_pid = self() + + stub_responses([ + {"issue(id: $id)", %{"data" => %{"issue" => issue_map()}}} + ]) + + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"query" => query} = Jason.decode!(body) + + cond do + String.contains?(query, "issue(id: $id)") -> + Req.Test.json(conn, %{"data" => %{"issue" => issue_map()}}) + + String.contains?(query, "states {") -> + Req.Test.json(conn, states_response()) + + String.contains?(query, "issueUpdate") -> + body_decoded = Jason.decode!(body) + send(test_pid, {:state_id, body_decoded["variables"]["input"]["stateId"]}) + Req.Test.json(conn, %{"data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}}}) + + true -> + raise "no stub matched query: #{query}" + end + end) + + output = + capture_io(fn -> + assert :ok = LinearCli.CLI.main(["issue", "status", "--status", "done", "CRY-1"]) + end) + + assert_received {:state_id, "s3"} + assert output =~ "CRY-1" + assert output =~ "status set to Done" + end + + test "-s short flag also sets the workflow state" do + test_pid = self() + + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"query" => query} = Jason.decode!(body) + + cond do + String.contains?(query, "issue(id: $id)") -> + Req.Test.json(conn, %{"data" => %{"issue" => issue_map()}}) + + String.contains?(query, "states {") -> + Req.Test.json(conn, states_response()) + + String.contains?(query, "issueUpdate") -> + body_decoded = Jason.decode!(body) + send(test_pid, {:state_id, body_decoded["variables"]["input"]["stateId"]}) + Req.Test.json(conn, %{"data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}}}) + + true -> + raise "no stub matched query: #{query}" + end + end) + + output = + capture_io(fn -> + assert :ok = LinearCli.CLI.main(["issue", "status", "-s", "Done", "CRY-1"]) + end) + + assert_received {:state_id, "s3"} + assert output =~ "status set to Done" + end + + test "--status with prefix match selects unique match" do + test_pid = self() + + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"query" => query} = Jason.decode!(body) + + cond do + String.contains?(query, "issue(id: $id)") -> + Req.Test.json(conn, %{"data" => %{"issue" => issue_map()}}) + + String.contains?(query, "states {") -> + Req.Test.json(conn, states_response()) + + String.contains?(query, "issueUpdate") -> + body_decoded = Jason.decode!(body) + send(test_pid, {:state_id, body_decoded["variables"]["input"]["stateId"]}) + Req.Test.json(conn, %{"data" => %{"issueUpdate" => %{"issue" => issue_with_state("s2", "In Progress")}}}) + + true -> + raise "no stub matched query: #{query}" + end + end) + + output = + capture_io(fn -> + assert :ok = LinearCli.CLI.main(["issue", "status", "--status", "in", "CRY-1"]) + end) + + assert_received {:state_id, "s2"} + assert output =~ "status set to In Progress" + end + + test "--status with unknown name exits 22 (smells bad)" do + test_pid = self() + halt = fn code -> send(test_pid, {:halted, code}) end + + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"query" => query} = Jason.decode!(body) + + cond do + String.contains?(query, "issue(id: $id)") -> + Req.Test.json(conn, %{"data" => %{"issue" => issue_map()}}) + + String.contains?(query, "states {") -> + Req.Test.json(conn, states_response()) + + true -> + raise "no stub matched query: #{query}" + end + end) + + stderr = + capture_io(:stderr, fn -> + LinearCli.CLI.main(["issue", "status", "--status", "Nonexistent", "CRY-1"], halt) + end) + + assert_received {:halted, 22} + assert stderr =~ "Unknown status" + assert stderr =~ "This smells bad! Bailing." + end + + test "--status with ambiguous prefix exits 22 (smells bad)" do + test_pid = self() + halt = fn code -> send(test_pid, {:halted, code}) end + + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"query" => query} = Jason.decode!(body) + + cond do + String.contains?(query, "issue(id: $id)") -> + Req.Test.json(conn, %{"data" => %{"issue" => issue_map()}}) + + String.contains?(query, "states {") -> + # Two states starting with "D" to trigger ambiguity + Req.Test.json(conn, + workflow_states([ + state_map("s1", "Done", 1.0, "completed"), + state_map("s2", "Doing", 2.0, "started") + ]) + ) + + true -> + raise "no stub matched query: #{query}" + end + end) + + stderr = + capture_io(:stderr, fn -> + LinearCli.CLI.main(["issue", "status", "--status", "Do", "CRY-1"], halt) + end) + + assert_received {:halted, 22} + assert stderr =~ "Ambiguous status" + assert stderr =~ "This smells bad! Bailing." + end + + test "--comment adds a comment before changing the status" do + test_pid = self() + + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"query" => query} = Jason.decode!(body) + + cond do + String.contains?(query, "issue(id: $id)") -> + Req.Test.json(conn, %{"data" => %{"issue" => issue_map()}}) + + String.contains?(query, "states {") -> + Req.Test.json(conn, states_response()) + + String.contains?(query, "commentCreate") -> + send(test_pid, :comment_created) + Req.Test.json(conn, comment_created()) + + String.contains?(query, "issueUpdate") -> + Req.Test.json(conn, %{"data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}}}) + + true -> + raise "no stub matched query: #{query}" + end + end) + + output = + capture_io(fn -> + assert :ok = + LinearCli.CLI.main([ + "issue", + "status", + "--status", + "Done", + "--comment", + "Wrapping up", + "CRY-1" + ]) + end) + + assert_received :comment_created + assert output =~ "Comment added to CRY-1" + assert output =~ "status set to Done" + end + + test "interactive selection (no --status) prompts from sorted states" do + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"query" => query} = Jason.decode!(body) + + cond do + String.contains?(query, "issue(id: $id)") -> + Req.Test.json(conn, %{"data" => %{"issue" => issue_map()}}) + + String.contains?(query, "states {") -> + Req.Test.json(conn, states_response()) + + String.contains?(query, "issueUpdate") -> + Req.Test.json(conn, %{"data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}}}) + + true -> + raise "no stub matched query: #{query}" + end + end) + + # Select the third option ("Done") interactively via stdin + output = + capture_io([input: "3\n"], fn -> + assert :ok = LinearCli.CLI.main(["issue", "status", "CRY-1"]) + end) + + assert output =~ "Choose a status" + assert output =~ "status set to Done" + end + + test "--output json emits structured output" do + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"query" => query} = Jason.decode!(body) + + cond do + String.contains?(query, "issue(id: $id)") -> + Req.Test.json(conn, %{"data" => %{"issue" => issue_map()}}) + + String.contains?(query, "states {") -> + Req.Test.json(conn, states_response()) + + String.contains?(query, "issueUpdate") -> + Req.Test.json(conn, %{"data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}}}) + + true -> + raise "no stub matched query: #{query}" + end + end) + + output = + capture_io(fn -> + assert :ok = + LinearCli.CLI.main([ + "issue", + "status", + "--status", + "Done", + "--output", + "json", + "CRY-1" + ]) + end) + + assert {:ok, decoded} = Jason.decode(output) + assert decoded["identifier"] == "CRY-1" + end + + test "alias 's' routes to issue status" do + test_pid = self() + + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"query" => query} = Jason.decode!(body) + + cond do + String.contains?(query, "issue(id: $id)") -> + Req.Test.json(conn, %{"data" => %{"issue" => issue_map()}}) + + String.contains?(query, "states {") -> + Req.Test.json(conn, states_response()) + + String.contains?(query, "issueUpdate") -> + send(test_pid, :updated) + Req.Test.json(conn, %{"data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}}}) + + true -> + raise "no stub matched query: #{query}" + end + end) + + capture_io(fn -> + assert :ok = LinearCli.CLI.main(["issue", "s", "--status", "Done", "CRY-1"]) + end) + + assert_received :updated + end + end + describe "issue update (Ruby: commands/issue/update.rb)" do test "--close comments with the given reason, then closes the issue" do stub_responses([ From 11d27c7eaddf71b9d44fe4a3029dd76fa33b14f5 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Sun, 16 Aug 2026 11:31:04 -0400 Subject: [PATCH 2/4] refactor(issue-status): decompose resolve_target_state into named pipeline steps Replace nested case statements with use_prefix_matches_if_empty/3 and resolve_state_matches/3 helpers, eliminating duplicate ambiguous-match branches and reducing cyclomatic complexity. Co-Authored-By: Claude Sonnet 4.6 --- app/lib/linear_cli/cli/commands.ex | 38 ++++++++++++++---------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/app/lib/linear_cli/cli/commands.ex b/app/lib/linear_cli/cli/commands.ex index 5f6623c..7fa98e6 100644 --- a/app/lib/linear_cli/cli/commands.ex +++ b/app/lib/linear_cli/cli/commands.ex @@ -506,32 +506,30 @@ defmodule LinearCli.CLI.Commands do end defp resolve_target_state(states, name) do - lower = String.downcase(name) + normalized_name = String.downcase(name) - case Enum.filter(states, &(String.downcase(&1.name) == lower)) do - [state] -> - {:ok, state} + states + |> Enum.filter(&(String.downcase(&1.name) == normalized_name)) + |> use_prefix_matches_if_empty(states, normalized_name) + |> resolve_state_matches(states, name) + end - [] -> - matches = Enum.filter(states, &String.starts_with?(String.downcase(&1.name), lower)) + defp use_prefix_matches_if_empty([], states, name) do + Enum.filter(states, &String.starts_with?(String.downcase(&1.name), name)) + end - case matches do - [state] -> - {:ok, state} + defp use_prefix_matches_if_empty(matches, _states, _name), do: matches - [] -> - available = Enum.map_join(states, ", ", & &1.name) - {:error, {:smells_bad, "Unknown status #{inspect(name)}. Available: #{available}"}} + defp resolve_state_matches([state], _states, _name), do: {:ok, state} - many -> - ambiguous = Enum.map_join(many, ", ", & &1.name) - {:error, {:smells_bad, "Ambiguous status #{inspect(name)}: matches #{ambiguous}"}} - end + defp resolve_state_matches([], states, name) do + available = Enum.map_join(states, ", ", & &1.name) + {:error, {:smells_bad, "Unknown status #{inspect(name)}. Available: #{available}"}} + end - many -> - ambiguous = Enum.map_join(many, ", ", & &1.name) - {:error, {:smells_bad, "Ambiguous status #{inspect(name)}: matches #{ambiguous}"}} - end + defp resolve_state_matches(matches, _states, name) do + ambiguous = Enum.map_join(matches, ", ", & &1.name) + {:error, {:smells_bad, "Ambiguous status #{inspect(name)}: matches #{ambiguous}"}} end defp maybe_add_status_comment(_issue, nil), do: :ok From d274a68ab769dd0b6e2b62ac5fd675c04ab7ae36 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Sun, 16 Aug 2026 11:37:14 -0400 Subject: [PATCH 3/4] style(issue-status): apply mix format to commands and test Co-Authored-By: Claude Sonnet 4.6 --- app/lib/linear_cli/cli/commands.ex | 5 ++- .../linear_cli/cli/issue_commands_test.exs | 35 ++++++++++++++----- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/app/lib/linear_cli/cli/commands.ex b/app/lib/linear_cli/cli/commands.ex index 7fa98e6..a91045d 100644 --- a/app/lib/linear_cli/cli/commands.ex +++ b/app/lib/linear_cli/cli/commands.ex @@ -495,7 +495,10 @@ defmodule LinearCli.CLI.Commands do :ok <- maybe_add_status_comment(issue, options.comment), {:ok, updated} <- Linear.set_issue_status(issue, target_state.id) do Display.show(updated, %{output: options.output}) - if options.output != "json", do: Prompt.ok("#{updated.identifier} status set to #{target_state.name}") + + if options.output != "json", + do: Prompt.ok("#{updated.identifier} status set to #{target_state.name}") + :ok end end diff --git a/app/test/linear_cli/cli/issue_commands_test.exs b/app/test/linear_cli/cli/issue_commands_test.exs index a6d31a1..9ed1f48 100644 --- a/app/test/linear_cli/cli/issue_commands_test.exs +++ b/app/test/linear_cli/cli/issue_commands_test.exs @@ -574,7 +574,10 @@ defmodule LinearCli.CLI.IssueCommandsTest do String.contains?(query, "issueUpdate") -> body_decoded = Jason.decode!(body) send(test_pid, {:state_id, body_decoded["variables"]["input"]["stateId"]}) - Req.Test.json(conn, %{"data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}}}) + + Req.Test.json(conn, %{ + "data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}} + }) true -> raise "no stub matched query: #{query}" @@ -608,7 +611,10 @@ defmodule LinearCli.CLI.IssueCommandsTest do String.contains?(query, "issueUpdate") -> body_decoded = Jason.decode!(body) send(test_pid, {:state_id, body_decoded["variables"]["input"]["stateId"]}) - Req.Test.json(conn, %{"data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}}}) + + Req.Test.json(conn, %{ + "data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}} + }) true -> raise "no stub matched query: #{query}" @@ -641,7 +647,10 @@ defmodule LinearCli.CLI.IssueCommandsTest do String.contains?(query, "issueUpdate") -> body_decoded = Jason.decode!(body) send(test_pid, {:state_id, body_decoded["variables"]["input"]["stateId"]}) - Req.Test.json(conn, %{"data" => %{"issueUpdate" => %{"issue" => issue_with_state("s2", "In Progress")}}}) + + Req.Test.json(conn, %{ + "data" => %{"issueUpdate" => %{"issue" => issue_with_state("s2", "In Progress")}} + }) true -> raise "no stub matched query: #{query}" @@ -701,7 +710,8 @@ defmodule LinearCli.CLI.IssueCommandsTest do String.contains?(query, "states {") -> # Two states starting with "D" to trigger ambiguity - Req.Test.json(conn, + Req.Test.json( + conn, workflow_states([ state_map("s1", "Done", 1.0, "completed"), state_map("s2", "Doing", 2.0, "started") @@ -742,7 +752,9 @@ defmodule LinearCli.CLI.IssueCommandsTest do Req.Test.json(conn, comment_created()) String.contains?(query, "issueUpdate") -> - Req.Test.json(conn, %{"data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}}}) + Req.Test.json(conn, %{ + "data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}} + }) true -> raise "no stub matched query: #{query}" @@ -781,7 +793,9 @@ defmodule LinearCli.CLI.IssueCommandsTest do Req.Test.json(conn, states_response()) String.contains?(query, "issueUpdate") -> - Req.Test.json(conn, %{"data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}}}) + Req.Test.json(conn, %{ + "data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}} + }) true -> raise "no stub matched query: #{query}" @@ -811,7 +825,9 @@ defmodule LinearCli.CLI.IssueCommandsTest do Req.Test.json(conn, states_response()) String.contains?(query, "issueUpdate") -> - Req.Test.json(conn, %{"data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}}}) + Req.Test.json(conn, %{ + "data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}} + }) true -> raise "no stub matched query: #{query}" @@ -852,7 +868,10 @@ defmodule LinearCli.CLI.IssueCommandsTest do String.contains?(query, "issueUpdate") -> send(test_pid, :updated) - Req.Test.json(conn, %{"data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}}}) + + Req.Test.json(conn, %{ + "data" => %{"issueUpdate" => %{"issue" => issue_with_state("s3", "Done")}} + }) true -> raise "no stub matched query: #{query}" From 0431f979580a95295b597cdac31a68d74d8fbaf4 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Sun, 16 Aug 2026 11:48:38 -0400 Subject: [PATCH 4/4] style(issue-status): remove review-noted clutter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop dead stub_responses call in first status test (immediately overridden by the manual Req.Test.stub below it). Strip temporal "New command." opener from @doc — it stops being accurate on day 2. Co-Authored-By: Claude Sonnet 4.6 --- app/lib/linear_cli/cli/commands.ex | 2 +- app/test/linear_cli/cli/issue_commands_test.exs | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/lib/linear_cli/cli/commands.ex b/app/lib/linear_cli/cli/commands.ex index a91045d..760f016 100644 --- a/app/lib/linear_cli/cli/commands.ex +++ b/app/lib/linear_cli/cli/commands.ex @@ -477,7 +477,7 @@ defmodule LinearCli.CLI.Commands do defp validate_issue_ids(_issue_ids), do: :ok @doc """ - New command. Changes the workflow state of an issue. + Changes the workflow state of an issue. With `--status`/`-s`, matches the given name against the issue's team's workflow states (case-insensitive exact, then unique prefix). Without it, diff --git a/app/test/linear_cli/cli/issue_commands_test.exs b/app/test/linear_cli/cli/issue_commands_test.exs index 9ed1f48..e123118 100644 --- a/app/test/linear_cli/cli/issue_commands_test.exs +++ b/app/test/linear_cli/cli/issue_commands_test.exs @@ -556,10 +556,6 @@ defmodule LinearCli.CLI.IssueCommandsTest do test "--status sets the workflow state by exact name (case-insensitive)" do test_pid = self() - stub_responses([ - {"issue(id: $id)", %{"data" => %{"issue" => issue_map()}}} - ]) - Req.Test.stub(LinearCli.Api, fn conn -> {:ok, body, conn} = Plug.Conn.read_body(conn) %{"query" => query} = Jason.decode!(body)