From b1d9d97b66d1ce8a6fab3978a964f2e013c6eb7b Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Mon, 24 Aug 2026 05:54:34 -0400 Subject: [PATCH] feat(issue-helpers): add move_issue/2 for moving an issue to a resolved project Add IssueHelpers.move_issue/2, which takes a pre-resolved %Project{} struct and calls Linear.attach_issue_to_project/2 with a confirmation message. Refactor attach_project/2 to delegate to move_issue/2 after resolving the search string, removing duplicated attachment logic. Co-Authored-By: Claude Sonnet 4.6 --- app/lib/linear_cli/cli/issue_helpers.ex | 34 ++++++++++++++----- .../linear_cli/cli/issue_helpers_test.exs | 28 +++++++++++++-- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/app/lib/linear_cli/cli/issue_helpers.ex b/app/lib/linear_cli/cli/issue_helpers.ex index 78ca577..e93dcca 100644 --- a/app/lib/linear_cli/cli/issue_helpers.ex +++ b/app/lib/linear_cli/cli/issue_helpers.ex @@ -282,6 +282,28 @@ defmodule LinearCli.CLI.IssueHelpers do :ok end + @doc """ + Moves `issue` to the already-resolved `project`, calling + `LinearCli.Linear.attach_issue_to_project/2` and printing a confirmation. + + Unlike `attach_project/2`, this function takes a pre-resolved + `%LinearCli.Linear.Project{}` struct rather than a search string. Callers + that need to resolve a search string first should use `attach_project/2`, + which delegates here after resolution. + """ + @spec move_issue(%Linear.Issue{}, %Linear.Project{}) :: + {:ok, %Linear.Issue{}} | {:error, term()} + def move_issue(issue, project) do + case Linear.attach_issue_to_project(issue, project.id) do + {:ok, updated} -> + Prompt.ok("#{issue.identifier} was moved to #{project.name}") + {:ok, updated} + + {:error, reason} -> + {:error, reason} + end + end + @doc """ Attaches `issue` to a project matched against `project_search` among its team's projects (`LinearCli.CLI.Projects.project_for/2`, prompting to @@ -291,6 +313,8 @@ defmodule LinearCli.CLI.IssueHelpers do `project_search` matching nothing in an empty project list (`project_for` returning `nil`) - the same faithfully-ported crash risk Ruby's own `nil.id` would hit. + + Resolves the project from the search string, then delegates to `move_issue/2`. """ @spec attach_project(%Linear.Issue{}, String.t() | nil) :: {:ok, %Linear.Issue{}} | {:error, term()} @@ -298,15 +322,7 @@ defmodule LinearCli.CLI.IssueHelpers do with {:ok, projects} <- Linear.projects_by_team(issue.team.id, %{search: project_search}) do project = Projects.project_for(projects, project_search) - - case Linear.attach_issue_to_project(issue, project.id) do - {:ok, updated} -> - Prompt.ok("#{issue.identifier} was attached to #{project.name}") - {:ok, updated} - - {:error, reason} -> - {:error, reason} - end + move_issue(issue, project) end end diff --git a/app/test/linear_cli/cli/issue_helpers_test.exs b/app/test/linear_cli/cli/issue_helpers_test.exs index 24d7316..5181e01 100644 --- a/app/test/linear_cli/cli/issue_helpers_test.exs +++ b/app/test/linear_cli/cli/issue_helpers_test.exs @@ -3,7 +3,7 @@ defmodule LinearCli.CLI.IssueHelpersTest do import ExUnit.CaptureIO alias LinearCli.CLI.IssueHelpers - alias LinearCli.Linear.{Comment, Issue, Team, User, WorkflowState} + alias LinearCli.Linear.{Comment, Issue, Project, Team, User, WorkflowState} # Every helper under test accepts an already-loaded resource struct (no # data-layer fetch happens inside these functions themselves, mirroring @@ -283,6 +283,28 @@ defmodule LinearCli.CLI.IssueHelpersTest do end end + describe "move_issue/2" do + test "moves the issue to the resolved project and prints a confirmation" do + stub_responses([{"issueUpdate", issue_updated()}]) + + project = %Project{id: "p1", name: "Manhattan Rollout"} + + assert capture_io(fn -> + assert {:ok, %Issue{}} = IssueHelpers.move_issue(issue(), project) + end) =~ "CRY-1 was moved to Manhattan Rollout" + end + + test "propagates an API error without printing confirmation" do + stub_responses([{"issueUpdate", %{"errors" => [%{"message" => "boom"}]}}]) + + project = %Project{id: "p1", name: "Manhattan Rollout"} + + assert capture_io(fn -> + assert {:error, %Ash.Error.Invalid{}} = IssueHelpers.move_issue(issue(), project) + end) == "" + end + end + describe "attach_project/2 (Ruby: CLI::Issue#attach_project)" do test "resolves the project by name against the team's projects and attaches it" do stub_responses([ @@ -303,7 +325,7 @@ defmodule LinearCli.CLI.IssueHelpersTest do assert capture_io(fn -> assert {:ok, %Issue{}} = IssueHelpers.attach_project(issue(), "Manhattan Rollout") - end) =~ "CRY-1 was attached to Manhattan Rollout" + end) =~ "CRY-1 was moved to Manhattan Rollout" end end @@ -380,7 +402,7 @@ defmodule LinearCli.CLI.IssueHelpersTest do assert :ok = IssueHelpers.update_issue(issue(), project: "Manhattan Rollout") end) - assert output =~ "CRY-1 was attached to Manhattan Rollout" + assert output =~ "CRY-1 was moved to Manhattan Rollout" end test "with :description, updates the issue description" do