From aa9dcf1900f7988f031141a66e7a728ffbce324d Mon Sep 17 00:00:00 2001 From: bougyman's bot Date: Fri, 11 Sep 2026 19:48:36 -0400 Subject: [PATCH] refactor(cli): extract issue creation and assignment workflows Moves `make_da_issue!/1` into `CLI.Issue.Creation` and `gimme_da_issue!/2` into `CLI.Issue.Assignment`, leaving `IssueHelpers` with only the remaining shared helpers. Updates all call sites in `Commands`, splits tests into focused files, and drops the moved tests from `IssueHelpersTest` and `ProfileDefaultsTest`. Co-Authored-By: Claude Sonnet 4.6 --- app/lib/linear_cli/cli/commands.ex | 30 +- app/lib/linear_cli/cli/issue/assignment.ex | 90 ++++ app/lib/linear_cli/cli/issue/creation.ex | 118 +++++ app/lib/linear_cli/cli/issue_helpers.ex | 172 +------ app/lib/linear_cli/profiles.ex | 2 +- .../linear_cli/cli/issue/assignment_test.exs | 407 +++++++++++++++ .../linear_cli/cli/issue/creation_test.exs | 96 ++++ .../linear_cli/cli/issue_helpers_test.exs | 479 +----------------- .../linear_cli/cli/profile_defaults_test.exs | 9 +- 9 files changed, 740 insertions(+), 663 deletions(-) create mode 100644 app/lib/linear_cli/cli/issue/assignment.ex create mode 100644 app/lib/linear_cli/cli/issue/creation.ex create mode 100644 app/test/linear_cli/cli/issue/assignment_test.exs create mode 100644 app/test/linear_cli/cli/issue/creation_test.exs diff --git a/app/lib/linear_cli/cli/commands.ex b/app/lib/linear_cli/cli/commands.ex index 6298ac5..def6e41 100644 --- a/app/lib/linear_cli/cli/commands.ex +++ b/app/lib/linear_cli/cli/commands.ex @@ -6,7 +6,7 @@ defmodule LinearCli.CLI.Commands do alias LinearCli.Browser alias LinearCli.CLI.{Display, IssueHelpers, Projects, Prompt, WhatFor} - alias LinearCli.CLI.Issue.{Actions, Identifiers} + alias LinearCli.CLI.Issue.{Actions, Assignment, Creation, Identifiers} alias LinearCli.{Favorites, Git, Linear, Profiles} @max_concurrent_issue_updates 20 @@ -346,7 +346,7 @@ defmodule LinearCli.CLI.Commands do @doc """ Ported from commands/issue/create.rb: resolves every field - (`LinearCli.CLI.IssueHelpers.make_da_issue!/1`), optionally self-assigns it + (`LinearCli.CLI.Issue.Creation.make_da_issue!/1`), optionally self-assigns it (`prompt.yes?('Do you want to take this issue?')`, unless `--no-take` was given), displays it, then, if `--dev` was given, chains straight into the same flow as `issue_develop/2` @@ -355,8 +355,8 @@ defmodule LinearCli.CLI.Commands do `opts` isn't part of Ruby's `call(**options)` arity - it exists purely to inject test doubles into whatever this command chains into: `:me` - (`gimme_da_issue!/2`, both for the self-assign prompt and, if `--dev` - fires, `run_develop/2`'s own re-fetch), `:cwd` + (`Assignment.gimme_da_issue!/2`, both for the self-assign prompt and, if + `--dev` fires, `run_develop/2`'s own re-fetch), `:cwd` (`LinearCli.Git.checkout_branch/2`/`pull_or_push_new_branch!/2`, only reached with `--dev`). Real callers (`LinearCli.CLI.main/2`) omit it. """ @@ -375,7 +375,7 @@ defmodule LinearCli.CLI.Commands do project: options.project, yes: flags.yes ], - {:ok, issue} <- IssueHelpers.make_da_issue!(create_opts), + {:ok, issue} <- Creation.make_da_issue!(create_opts), :ok <- maybe_take(issue, flags, opts) do Display.show(issue, %{output: options.output}) if flags.develop, do: run_develop(issue.id, opts), else: :ok @@ -390,7 +390,7 @@ defmodule LinearCli.CLI.Commands do defp maybe_take(_issue, %{no_take: true}, _opts), do: :ok defp maybe_take(issue, %{yes: true}, opts) do - case IssueHelpers.gimme_da_issue!(issue.id, opts) do + case Assignment.gimme_da_issue!(issue.id, opts) do {:ok, _updated} -> :ok {:error, reason} -> {:error, reason} end @@ -398,7 +398,7 @@ defmodule LinearCli.CLI.Commands do defp maybe_take(issue, _flags, opts) do if Prompt.yes?("Do you want to take this issue?") do - case IssueHelpers.gimme_da_issue!(issue.id, opts) do + case Assignment.gimme_da_issue!(issue.id, opts) do {:ok, _updated} -> :ok {:error, reason} -> {:error, reason} end @@ -409,7 +409,7 @@ defmodule LinearCli.CLI.Commands do @doc """ Ported from commands/issue/develop.rb: resolves/self-assigns `issue_id` - (`LinearCli.CLI.IssueHelpers.gimme_da_issue!/2`), checks out its + (`LinearCli.CLI.Issue.Assignment.gimme_da_issue!/2`), checks out its `branch_name` (creating it first if it doesn't exist locally yet), then pulls it (or, if there's no upstream tracking branch yet, pushes it to `origin` and sets one up). @@ -417,8 +417,8 @@ defmodule LinearCli.CLI.Commands do `opts` (this port's addition, not part of Ruby's `call(issue_id:, **options)`) forwards to `LinearCli.Git.checkout_branch/2`/ `pull_or_push_new_branch!/2` (`:cwd`) and - `LinearCli.CLI.IssueHelpers.gimme_da_issue!/2` (`:me`) - pass overrides in - tests so this never shells out to real git or hits a real `viewer` query; + `LinearCli.CLI.Issue.Assignment.gimme_da_issue!/2` (`:me`) - pass overrides + in tests so this never shells out to real git or hits a real `viewer` query; real callers omit it. """ @spec issue_develop(Optimus.ParseResult.t(), keyword()) :: :ok | {:error, term()} @@ -426,7 +426,7 @@ defmodule LinearCli.CLI.Commands do def issue_develop(%{args: %{issue_id: issue_id}}, opts), do: run_develop(issue_id, opts) defp run_develop(issue_id, opts) do - with {:ok, issue} <- IssueHelpers.gimme_da_issue!(issue_id, opts), + with {:ok, issue} <- Assignment.gimme_da_issue!(issue_id, opts), {:ok, _branch} <- Git.checkout_branch(issue.branch_name, opts) do Prompt.ok("Checked out branch #{issue.branch_name}") finish_pull_or_push(issue.branch_name, opts) @@ -469,7 +469,7 @@ defmodule LinearCli.CLI.Commands do def issue_pr(result, opts \\ []) def issue_pr(%{args: %{issue_id: issue_id}, options: options}, opts) do - with {:ok, issue} <- IssueHelpers.gimme_da_issue!(issue_id, opts), + with {:ok, issue} <- Assignment.gimme_da_issue!(issue_id, opts), {:ok, _branch} <- Git.checkout_branch(issue.branch_name, opts) do Prompt.ok("Checked out branch #{issue.branch_name}") @@ -495,8 +495,8 @@ defmodule LinearCli.CLI.Commands do its `filter_map`. `opts` (this port's addition) forwards to - `LinearCli.CLI.IssueHelpers.gimme_da_issue!/2` (`:me`); real callers omit - it. + `LinearCli.CLI.Issue.Assignment.gimme_da_issue!/2` (`:me`); real callers + omit it. """ @spec issue_take(Optimus.ParseResult.t(), keyword()) :: :ok | {:error, term()} def issue_take(result, opts \\ []) @@ -516,7 +516,7 @@ defmodule LinearCli.CLI.Commands do defp take_issues(issue_ids, opts) do issue_ids |> Enum.reduce_while({:ok, []}, fn issue_id, {:ok, acc} -> - case IssueHelpers.gimme_da_issue!(issue_id, opts) do + case Assignment.gimme_da_issue!(issue_id, opts) do {:ok, issue} -> {:cont, {:ok, [issue | acc]}} diff --git a/app/lib/linear_cli/cli/issue/assignment.ex b/app/lib/linear_cli/cli/issue/assignment.ex new file mode 100644 index 0000000..4ad4afe --- /dev/null +++ b/app/lib/linear_cli/cli/issue/assignment.ex @@ -0,0 +1,90 @@ +defmodule LinearCli.CLI.Issue.Assignment do + @moduledoc """ + Issue self-assignment with optional workflow-state transition. + + Extracted from `LinearCli.CLI.IssueHelpers`. The single public function, + `gimme_da_issue!/2`, looks up an issue by identifier and self-assigns it + to the current user, unless already assigned. Accepts an optional + `--status` option (or an already-resolved `:state_id`) to simultaneously + transition the issue's workflow state. + + Reuses `LinearCli.CLI.Issue.Identifiers.expand_issue_id/1` for bare-ID + expansion and `LinearCli.CLI.Issue.WorkflowStates.resolve_workflow_state/2` + for status-name matching, keeping the shared logic in one place. + + Ported from `CLI::Issue#gimme_da_issue!`. + + ## Return convention + + Returns `{:ok, issue}` on success or `{:error, reason}` on failure (never + raises). User-visible failures use `{:error, {:smells_bad, message}}`, + matching the convention established by `LinearCli.CLI.IssueHelpers`. + """ + + alias LinearCli.CLI.Issue.{Identifiers, WorkflowStates} + alias LinearCli.CLI.Prompt + alias LinearCli.Linear + + @doc """ + Looks up `issue_id` and self-assigns it to the caller, unless it's already + assigned to them. + + `opts[:me]` overrides the caller lookup (this port's stand-in for Ruby's + `me: Rubyists::Linear::User.me` keyword default) - real callers omit it + and get `LinearCli.Linear.me/0`; tests pass it to avoid stubbing the + `viewer` query too. + + `opts[:status]` accepts a case-insensitive exact or unique-prefix workflow + state name to transition the issue at the same time as assignment. + `opts[:state_id]` accepts an already-resolved state ID directly. + + Ported from `CLI::Issue#gimme_da_issue!`. + """ + @spec gimme_da_issue!(String.t(), keyword()) :: {:ok, %Linear.Issue{}} | {:error, term()} + def gimme_da_issue!(issue_id, opts \\ []) do + issue_id = Identifiers.expand_issue_id(issue_id) + status_opt = parse_status_opt(opts) + + with {:ok, me} <- resolve_me(opts), + {:ok, [issue]} <- Linear.issues(%{ids: [issue_id]}), + {:ok, state_id} <- resolve_status_for_issue(issue, status_opt) do + assign_or_confirm(issue, me, issue_id, state_id) + end + end + + defp parse_status_opt(opts) do + case Keyword.fetch(opts, :state_id) do + {:ok, id} -> {:resolved, id} + :error -> {:name, Keyword.get(opts, :status)} + end + end + + defp resolve_status_for_issue(_issue, {:resolved, id}), do: {:ok, id} + defp resolve_status_for_issue(_issue, {:name, nil}), do: {:ok, nil} + + defp resolve_status_for_issue(issue, {:name, name}) do + with {:ok, states} <- Linear.workflow_states_by_team(issue.team.id) do + case WorkflowStates.resolve_workflow_state(states, name) do + {:ok, state} -> {:ok, state.id} + error -> error + end + end + end + + defp assign_or_confirm(%{assignee: %{id: id}} = issue, %{id: id}, issue_id, nil) do + Prompt.say("You are already assigned #{issue_id}") + {:ok, issue} + end + + defp assign_or_confirm(issue, me, issue_id, state_id) do + Prompt.say("Assigning issue #{issue_id} to ya") + Linear.assign_issue(issue, me.id, %{state_id: state_id}) + end + + defp resolve_me(opts) do + case Keyword.fetch(opts, :me) do + {:ok, me} -> {:ok, me} + :error -> Linear.me() + end + end +end diff --git a/app/lib/linear_cli/cli/issue/creation.ex b/app/lib/linear_cli/cli/issue/creation.ex new file mode 100644 index 0000000..44fdda7 --- /dev/null +++ b/app/lib/linear_cli/cli/issue/creation.ex @@ -0,0 +1,118 @@ +defmodule LinearCli.CLI.Issue.Creation do + @moduledoc """ + Interactive and non-interactive issue creation. + + Extracted from `LinearCli.CLI.IssueHelpers`. The single public function, + `make_da_issue!/1`, creates a new Linear issue by resolving title, + description, team, labels, and project either interactively (when `--yes` + is not given) or strictly from provided options (when `--yes` is set). + + Profile defaults (active team/project) are consulted before interactive + prompting when options are omitted. + + Ported from `CLI::Issue#make_da_issue!`. + + ## Return convention + + Returns `{:ok, issue}` on success or `{:error, reason}` on failure (never + raises). User-visible failures use `{:error, {:smells_bad, message}}`, + matching the convention established by `LinearCli.CLI.IssueHelpers`. + """ + + alias LinearCli.CLI.{Projects, WhatFor} + alias LinearCli.{Linear, Profiles} + + @doc """ + Creates a new issue, resolving every field that wasn't already given in + `opts` interactively (title, description, team, labels, project - via + `LinearCli.CLI.WhatFor`/`LinearCli.CLI.Projects`). + + `opts` (Ruby's `**options`): `:title`, `:description`, `:team`, `:labels`, + `:project`. `:team`/`:project`, if omitted, fall back to + `LinearCli.Profiles.default_team/0`/`default_project/0` (the active + profile, if any) before `WhatFor.team_for/1`/`Projects.project_for/2`'s + own interactive prompting kicks in. + + When `opts[:yes]` is truthy, all fields must be supplied (no interactive + prompts); missing required fields return `{:error, {:smells_bad, message}}`. + + Ported from `CLI::Issue#make_da_issue!`. + """ + @spec make_da_issue!(keyword()) :: {:ok, %Linear.Issue{}} | {:error, term()} + def make_da_issue!(opts \\ []) do + if opts[:yes], do: make_da_issue_no_prompts!(opts), else: make_da_issue_interactive!(opts) + end + + defp make_da_issue_interactive!(opts) do + title = WhatFor.title_for(opts[:title]) + description = WhatFor.description_for(opts[:description]) + team = WhatFor.team_for(opts[:team] || Profiles.default_team()) + labels = WhatFor.labels_for(team, opts[:labels]) + project_search = opts[:project] || Profiles.default_project() + + with {:ok, projects} <- Linear.projects_by_team(team.id, %{search: project_search}) do + project = Projects.project_for(projects, project_search) + label_ids = Enum.map(labels, & &1.id) + params = maybe_put_project_id(%{label_ids: label_ids}, project) + + Linear.create_issue(title, description, team.id, params) + end + end + + defp make_da_issue_no_prompts!(opts) do + with {:ok, title} <- require_field(opts[:title], "--title"), + {:ok, description} <- require_field(opts[:description], "--description"), + {:ok, team} <- resolve_team_strict(opts[:team] || Profiles.default_team()) do + labels = + case opts[:labels] do + nil -> [] + [] -> [] + labels -> WhatFor.labels_for(team, labels) + end + + project_search = opts[:project] || Profiles.default_project() + + with {:ok, projects} <- Linear.projects_by_team(team.id, %{search: project_search}) do + project = + if project_search, + do: Projects.project_for_strict(projects, project_search), + else: nil + + label_ids = Enum.map(labels, & &1.id) + params = maybe_put_project_id(%{label_ids: label_ids}, project) + Linear.create_issue(title, description, team.id, params) + end + end + end + + defp require_field(nil, flag), + do: {:error, {:smells_bad, "#{flag} is required with --yes"}} + + defp require_field(value, _flag), do: {:ok, value} + + defp resolve_team_strict(nil) do + case Linear.my_teams() do + {:ok, [team]} -> + {:ok, team} + + {:ok, []} -> + {:error, {:smells_bad, "--team is required (you belong to no teams)"}} + + {:ok, _teams} -> + {:error, {:smells_bad, "--team is required when you belong to multiple teams"}} + + {:error, reason} -> + {:error, {:smells_bad, "Could not fetch teams: #{inspect(reason)}"}} + end + end + + defp resolve_team_strict(key) do + case Linear.find_team(key) do + {:ok, team} -> {:ok, team} + {:error, _reason} -> {:error, {:smells_bad, "--team #{inspect(key)} not found"}} + end + end + + defp maybe_put_project_id(params, nil), do: params + defp maybe_put_project_id(params, project), do: Map.put(params, :project_id, project.id) +end diff --git a/app/lib/linear_cli/cli/issue_helpers.ex b/app/lib/linear_cli/cli/issue_helpers.ex index 3752eb5..5968eb1 100644 --- a/app/lib/linear_cli/cli/issue_helpers.ex +++ b/app/lib/linear_cli/cli/issue_helpers.ex @@ -1,16 +1,18 @@ defmodule LinearCli.CLI.IssueHelpers do @moduledoc """ - Shared issue-command helpers - open a PR, create, self-assign. + Shared issue-command helpers - open a PR. Ported from `Rubyists::Linear::CLI::Issue` (vendor/ruby-linear-cli/lib/linear/commands/issue.rb): `create_pr!`, - `issue_pr`, `make_da_issue!`, `gimme_da_issue!`. + `issue_pr`. Lifecycle mutations (comment, close/cancel, description update, project attachment/move, and update-dispatch) have been extracted to `LinearCli.CLI.Issue.Actions`. Bare-ID expansion lives in `LinearCli.CLI.Issue.Identifiers`. Workflow-state selection lives in - `LinearCli.CLI.Issue.WorkflowStates`. + `LinearCli.CLI.Issue.WorkflowStates`. Issue creation lives in + `LinearCli.CLI.Issue.Creation`. Self-assignment lives in + `LinearCli.CLI.Issue.Assignment`. ## Return convention @@ -26,15 +28,6 @@ defmodule LinearCli.CLI.IssueHelpers do where `message` is a human-readable `String.t()`. - ## Project lookups - - `make_da_issue!/1` (Ruby: `team.projects`) needs a team's projects. Neither - `LinearCli.Linear.Issue` nor `LinearCli.Linear.Team` stores a `:projects` - field on their structs (Team's own GraphQL `full_fields/0` embeds a - `projects` sub-selection, but `Team.from_map/1` never parses it into an - attribute - there's nowhere on the struct to put it), so it calls - `LinearCli.Linear.projects_by_team/1` domain interface instead. - ## `create_pr!/3` Ported from `CLI::Issue#create_pr!`, which shells out to `gh pr create`. @@ -49,9 +42,8 @@ defmodule LinearCli.CLI.IssueHelpers do `LinearCli.Git`'s injectable `cwd:`. """ - alias LinearCli.CLI.Issue.{Identifiers, WorkflowStates} - alias LinearCli.CLI.{Projects, Prompt, WhatFor} - alias LinearCli.{Linear, Profiles} + alias LinearCli.CLI.{Prompt, WhatFor} + alias LinearCli.Linear @doc """ Shells out to `gh pr create -a @me --title TITLE --body BODY`, returning @@ -100,154 +92,4 @@ defmodule LinearCli.CLI.IssueHelpers do Prompt.warn(create_pr!(title, body, runner)) :ok end - - @doc """ - Creates a new issue, resolving every field that wasn't already given in - `opts` interactively (title, description, team, labels, project - via - `LinearCli.CLI.WhatFor`/`LinearCli.CLI.Projects`). - - `opts` (Ruby's `**options`): `:title`, `:description`, `:team`, `:labels`, - `:project`. `:team`/`:project`, if omitted, fall back to - `LinearCli.Profiles.default_team/0`/`default_project/0` (the active - profile, if any) before `WhatFor.team_for/1`/`Projects.project_for/2`'s - own interactive prompting kicks in. - - Ported from `CLI::Issue#make_da_issue!`. - """ - @spec make_da_issue!(keyword()) :: {:ok, %Linear.Issue{}} | {:error, term()} - def make_da_issue!(opts \\ []) do - if opts[:yes], do: make_da_issue_no_prompts!(opts), else: make_da_issue_interactive!(opts) - end - - defp make_da_issue_interactive!(opts) do - title = WhatFor.title_for(opts[:title]) - description = WhatFor.description_for(opts[:description]) - team = WhatFor.team_for(opts[:team] || Profiles.default_team()) - labels = WhatFor.labels_for(team, opts[:labels]) - project_search = opts[:project] || Profiles.default_project() - - with {:ok, projects} <- Linear.projects_by_team(team.id, %{search: project_search}) do - project = Projects.project_for(projects, project_search) - label_ids = Enum.map(labels, & &1.id) - params = maybe_put_project_id(%{label_ids: label_ids}, project) - - Linear.create_issue(title, description, team.id, params) - end - end - - defp make_da_issue_no_prompts!(opts) do - with {:ok, title} <- require_field(opts[:title], "--title"), - {:ok, description} <- require_field(opts[:description], "--description"), - {:ok, team} <- resolve_team_strict(opts[:team] || Profiles.default_team()) do - labels = - case opts[:labels] do - nil -> [] - [] -> [] - labels -> WhatFor.labels_for(team, labels) - end - - project_search = opts[:project] || Profiles.default_project() - - with {:ok, projects} <- Linear.projects_by_team(team.id, %{search: project_search}) do - project = - if project_search, - do: Projects.project_for_strict(projects, project_search), - else: nil - - label_ids = Enum.map(labels, & &1.id) - params = maybe_put_project_id(%{label_ids: label_ids}, project) - Linear.create_issue(title, description, team.id, params) - end - end - end - - defp require_field(nil, flag), - do: {:error, {:smells_bad, "#{flag} is required with --yes"}} - - defp require_field(value, _flag), do: {:ok, value} - - defp resolve_team_strict(nil) do - case Linear.my_teams() do - {:ok, [team]} -> - {:ok, team} - - {:ok, []} -> - {:error, {:smells_bad, "--team is required (you belong to no teams)"}} - - {:ok, _teams} -> - {:error, {:smells_bad, "--team is required when you belong to multiple teams"}} - - {:error, reason} -> - {:error, {:smells_bad, "Could not fetch teams: #{inspect(reason)}"}} - end - end - - defp resolve_team_strict(key) do - case Linear.find_team(key) do - {:ok, team} -> {:ok, team} - {:error, _reason} -> {:error, {:smells_bad, "--team #{inspect(key)} not found"}} - end - end - - defp maybe_put_project_id(params, nil), do: params - defp maybe_put_project_id(params, project), do: Map.put(params, :project_id, project.id) - - @doc """ - Looks up `issue_id` and self-assigns it to the caller, unless it's already - assigned to them. - - `opts[:me]` overrides the caller lookup (this port's stand-in for Ruby's - `me: Rubyists::Linear::User.me` keyword default) - real callers omit it - and get `LinearCli.Linear.me/0`; tests pass it to avoid stubbing the - `viewer` query too. - - Ported from `CLI::Issue#gimme_da_issue!`. - """ - @spec gimme_da_issue!(String.t(), keyword()) :: {:ok, %Linear.Issue{}} | {:error, term()} - def gimme_da_issue!(issue_id, opts \\ []) do - issue_id = Identifiers.expand_issue_id(issue_id) - status_opt = parse_status_opt(opts) - - with {:ok, me} <- resolve_me(opts), - {:ok, [issue]} <- Linear.issues(%{ids: [issue_id]}), - {:ok, state_id} <- resolve_status_for_issue(issue, status_opt) do - assign_or_confirm(issue, me, issue_id, state_id) - end - end - - defp parse_status_opt(opts) do - case Keyword.fetch(opts, :state_id) do - {:ok, id} -> {:resolved, id} - :error -> {:name, Keyword.get(opts, :status)} - end - end - - defp resolve_status_for_issue(_issue, {:resolved, id}), do: {:ok, id} - defp resolve_status_for_issue(_issue, {:name, nil}), do: {:ok, nil} - - defp resolve_status_for_issue(issue, {:name, name}) do - with {:ok, states} <- Linear.workflow_states_by_team(issue.team.id) do - case WorkflowStates.resolve_workflow_state(states, name) do - {:ok, state} -> {:ok, state.id} - error -> error - end - end - end - - defp assign_or_confirm(%{assignee: %{id: id}} = issue, %{id: id}, issue_id, nil) do - Prompt.say("You are already assigned #{issue_id}") - {:ok, issue} - end - - defp assign_or_confirm(issue, me, issue_id, state_id) do - Prompt.say("Assigning issue #{issue_id} to ya") - Linear.assign_issue(issue, me.id, %{state_id: state_id}) - end - - defp resolve_me(opts) do - case Keyword.fetch(opts, :me) do - {:ok, me} -> {:ok, me} - :error -> Linear.me() - end - end end diff --git a/app/lib/linear_cli/profiles.ex b/app/lib/linear_cli/profiles.ex index eb0a70a..266a3a0 100644 --- a/app/lib/linear_cli/profiles.ex +++ b/app/lib/linear_cli/profiles.ex @@ -5,7 +5,7 @@ defmodule LinearCli.Profiles do `config/runtime.exs`'s `:profiles_db_path`), with at most one active at a time - enforced by a partial unique index (`active_idx`), not application-level bookkeeping. `default_team/0`/`default_project/0` are - what `LinearCli.CLI.IssueHelpers.make_da_issue!/1` and + what `LinearCli.CLI.Issue.Creation.make_da_issue!/1` and `LinearCli.CLI.Commands.issue_list/1` fall back to when `--team`/ `--project` are omitted - see `documents/phase-9-plan.adoc`. diff --git a/app/test/linear_cli/cli/issue/assignment_test.exs b/app/test/linear_cli/cli/issue/assignment_test.exs new file mode 100644 index 0000000..df4c161 --- /dev/null +++ b/app/test/linear_cli/cli/issue/assignment_test.exs @@ -0,0 +1,407 @@ +defmodule LinearCli.CLI.Issue.AssignmentTest do + use ExUnit.Case, async: true + import ExUnit.CaptureIO + + alias LinearCli.CLI.Issue.Assignment + alias LinearCli.Linear.{Issue, User} + + defp stub_responses(pairs) do + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"query" => query} = Jason.decode!(body) + + case Enum.find(pairs, fn {match, _resp} -> String.contains?(query, match) end) do + {_match, response} -> Req.Test.json(conn, response) + nil -> raise "no stub matched query: #{query}" + end + end) + end + + defp issue_updated(overrides) do + issue_map = + Map.merge( + %{ + "id" => "i1", + "identifier" => "CRY-1", + "title" => "Fix the thing", + "branchName" => "cry-1-fix-the-thing", + "description" => "It is broken", + "assignee" => nil, + "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"}, + "comments" => %{"nodes" => []} + }, + overrides + ) + + %{"data" => %{"issueUpdate" => %{"issue" => issue_map}}} + end + + describe "gimme_da_issue!/2 (Ruby: CLI::Issue#gimme_da_issue!)" do + test "when already assigned to the caller, says so and doesn't reassign" do + stub_responses([ + {"issue(id: $id)", + %{ + "data" => %{ + "issue" => %{ + "id" => "i1", + "identifier" => "CRY-1", + "title" => "Fix the thing", + "branchName" => "cry-1-fix-the-thing", + "description" => nil, + "assignee" => %{ + "id" => "u1", + "name" => "Ada", + "email" => "ada@x.com", + "teams" => %{"nodes" => []} + }, + "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"}, + "comments" => %{"nodes" => []} + } + } + }} + ]) + + me = %User{id: "u1", name: "Ada", email: "ada@x.com"} + + assert capture_io(fn -> + assert {:ok, %Issue{identifier: "CRY-1"}} = + Assignment.gimme_da_issue!("CRY-1", me: me) + end) =~ "You are already assigned CRY-1" + end + + test "when unassigned, self-assigns" do + stub_responses([ + {"issue(id: $id)", + %{ + "data" => %{ + "issue" => %{ + "id" => "i1", + "identifier" => "CRY-1", + "title" => "Fix the thing", + "branchName" => "cry-1-fix-the-thing", + "description" => nil, + "assignee" => nil, + "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"}, + "comments" => %{"nodes" => []} + } + } + }}, + {"issueUpdate", + issue_updated(%{ + "assignee" => %{ + "id" => "u1", + "name" => "Ada", + "email" => "ada@x.com", + "teams" => %{"nodes" => []} + } + })} + ]) + + me = %User{id: "u1", name: "Ada", email: "ada@x.com"} + + output = + capture_io(fn -> + assert {:ok, %Issue{assignee: %User{id: "u1"}}} = + Assignment.gimme_da_issue!("CRY-1", me: me) + end) + + assert output =~ "Assigning issue CRY-1 to ya" + end + + test "when already assigned to someone else, self-assigns" do + stub_responses([ + {"issue(id: $id)", + %{ + "data" => %{ + "issue" => %{ + "id" => "i1", + "identifier" => "CRY-1", + "title" => "Fix the thing", + "branchName" => "cry-1-fix-the-thing", + "description" => nil, + "assignee" => %{ + "id" => "u2", + "name" => "Bob", + "email" => "bob@x.com", + "teams" => %{"nodes" => []} + }, + "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"}, + "comments" => %{"nodes" => []} + } + } + }}, + {"issueUpdate", + issue_updated(%{ + "assignee" => %{ + "id" => "u1", + "name" => "Ada", + "email" => "ada@x.com", + "teams" => %{"nodes" => []} + } + })} + ]) + + me = %User{id: "u1", name: "Ada", email: "ada@x.com"} + + assert capture_io(fn -> + assert {:ok, %Issue{assignee: %User{id: "u1"}}} = + Assignment.gimme_da_issue!("CRY-1", me: me) + end) =~ "Assigning issue CRY-1 to ya" + end + + test "with status: opt, resolves state per team and sends stateId" do + stub_responses([ + {"issue(id: $id)", + %{ + "data" => %{ + "issue" => %{ + "id" => "i1", + "identifier" => "CRY-1", + "title" => "Fix the thing", + "branchName" => "cry-1-fix-the-thing", + "description" => nil, + "assignee" => nil, + "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"}, + "comments" => %{"nodes" => []} + } + } + }}, + {"states {", + %{ + "data" => %{ + "team" => %{ + "states" => %{ + "nodes" => [ + %{ + "id" => "s1", + "name" => "In Progress", + "position" => 1.0, + "type" => "started", + "description" => nil + } + ] + } + } + } + }}, + {"issueUpdate", + issue_updated(%{ + "assignee" => %{ + "id" => "u1", + "name" => "Ada", + "email" => "ada@x.com", + "teams" => %{"nodes" => []} + } + })} + ]) + + me = %User{id: "u1", name: "Ada", email: "ada@x.com"} + + capture_io(fn -> + assert {:ok, %Issue{identifier: "CRY-1"}} = + Assignment.gimme_da_issue!("CRY-1", me: me, status: "In Progress") + end) + end + + test "with status: opt, case-insensitive match sends correct stateId" do + test_pid = self() + + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + decoded = Jason.decode!(body) + %{"query" => query} = decoded + + cond do + String.contains?(query, "issue(id: $id)") -> + Req.Test.json(conn, %{ + "data" => %{ + "issue" => %{ + "id" => "i1", + "identifier" => "CRY-1", + "title" => "Fix the thing", + "branchName" => "cry-1-fix-the-thing", + "description" => nil, + "assignee" => nil, + "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"}, + "comments" => %{"nodes" => []} + } + } + }) + + String.contains?(query, "states {") -> + Req.Test.json(conn, %{ + "data" => %{ + "team" => %{ + "states" => %{ + "nodes" => [ + %{ + "id" => "s99", + "name" => "Todo", + "position" => 0.0, + "type" => "unstarted", + "description" => nil + } + ] + } + } + } + }) + + String.contains?(query, "issueUpdate") -> + send(test_pid, {:input, decoded["variables"]["input"]}) + + Req.Test.json( + conn, + issue_updated(%{ + "assignee" => %{ + "id" => "u1", + "name" => "Ada", + "email" => "ada@x.com", + "teams" => %{"nodes" => []} + } + }) + ) + + true -> + raise "no stub matched: #{query}" + end + end) + + me = %User{id: "u1", name: "Ada", email: "ada@x.com"} + + capture_io(fn -> + assert {:ok, _} = Assignment.gimme_da_issue!("CRY-1", me: me, status: "todo") + end) + + assert_received {:input, input} + assert input["stateId"] == "s99" + end + + test "with status: opt, already-assigned still sends stateId mutation" do + test_pid = self() + + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + decoded = Jason.decode!(body) + %{"query" => query} = decoded + + cond do + String.contains?(query, "issue(id: $id)") -> + Req.Test.json(conn, %{ + "data" => %{ + "issue" => %{ + "id" => "i1", + "identifier" => "CRY-1", + "title" => "Fix the thing", + "branchName" => "cry-1-fix-the-thing", + "description" => nil, + "assignee" => %{ + "id" => "u1", + "name" => "Ada", + "email" => "ada@x.com", + "teams" => %{"nodes" => []} + }, + "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"}, + "comments" => %{"nodes" => []} + } + } + }) + + String.contains?(query, "states {") -> + Req.Test.json(conn, %{ + "data" => %{ + "team" => %{ + "states" => %{ + "nodes" => [ + %{ + "id" => "s2", + "name" => "In Progress", + "position" => 1.0, + "type" => "started", + "description" => nil + } + ] + } + } + } + }) + + String.contains?(query, "issueUpdate") -> + send(test_pid, {:input, decoded["variables"]["input"]}) + + Req.Test.json( + conn, + issue_updated(%{ + "assignee" => %{ + "id" => "u1", + "name" => "Ada", + "email" => "ada@x.com", + "teams" => %{"nodes" => []} + } + }) + ) + + true -> + raise "no stub matched: #{query}" + end + end) + + me = %User{id: "u1", name: "Ada", email: "ada@x.com"} + + capture_io(fn -> + assert {:ok, _} = Assignment.gimme_da_issue!("CRY-1", me: me, status: "In Progress") + end) + + assert_received {:input, input} + assert input["assigneeId"] == "u1" + assert input["stateId"] == "s2" + end + + test "with status: opt, unknown name returns smells_bad error" do + stub_responses([ + {"issue(id: $id)", + %{ + "data" => %{ + "issue" => %{ + "id" => "i1", + "identifier" => "CRY-1", + "title" => "Fix the thing", + "branchName" => "cry-1-fix-the-thing", + "description" => nil, + "assignee" => nil, + "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"}, + "comments" => %{"nodes" => []} + } + } + }}, + {"states {", + %{ + "data" => %{ + "team" => %{ + "states" => %{ + "nodes" => [ + %{ + "id" => "s1", + "name" => "Todo", + "position" => 0.0, + "type" => "unstarted", + "description" => nil + } + ] + } + } + } + }} + ]) + + me = %User{id: "u1", name: "Ada", email: "ada@x.com"} + + capture_io(fn -> + assert {:error, {:smells_bad, msg}} = + Assignment.gimme_da_issue!("CRY-1", me: me, status: "NoSuch") + + assert msg =~ "Unknown status" + end) + end + end +end diff --git a/app/test/linear_cli/cli/issue/creation_test.exs b/app/test/linear_cli/cli/issue/creation_test.exs new file mode 100644 index 0000000..285f082 --- /dev/null +++ b/app/test/linear_cli/cli/issue/creation_test.exs @@ -0,0 +1,96 @@ +defmodule LinearCli.CLI.Issue.CreationTest do + use ExUnit.Case, async: true + import ExUnit.CaptureIO + + alias LinearCli.CLI.Issue.Creation + alias LinearCli.Linear.Issue + + defp stub_responses(pairs) do + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"query" => query} = Jason.decode!(body) + + case Enum.find(pairs, fn {match, _resp} -> String.contains?(query, match) end) do + {_match, response} -> Req.Test.json(conn, response) + nil -> raise "no stub matched query: #{query}" + end + end) + end + + defp team_projects(projects) do + %{"data" => %{"team" => %{"projects" => %{"nodes" => projects}}}} + end + + describe "make_da_issue!/1 (Ruby: CLI::Issue#make_da_issue!)" do + test "creates the issue with resolved title/description/team/labels/project" do + stub_responses([ + {"team(id: $id)", + %{ + "data" => %{ + "team" => %{ + "id" => "t1", + "key" => "ENG", + "name" => "Engineering", + "description" => nil + } + } + }}, + {"issueLabels", + %{ + "data" => %{ + "issueLabels" => %{ + "edges" => [ + %{ + "node" => %{ + "id" => "l1", + "name" => "urgent", + "description" => nil, + "isGroup" => false + } + } + ] + } + } + }}, + {"projects(first: 100", + team_projects([ + %{ + "id" => "p1", + "name" => "Manhattan Rollout", + "content" => nil, + "slugId" => "abc", + "description" => nil, + "url" => "https://linear.app/x/project/manhattan-rollout-abc" + } + ])}, + {"issueCreate", + %{ + "data" => %{ + "issueCreate" => %{ + "issue" => %{ + "id" => "i2", + "identifier" => "CRY-2", + "title" => "New thing", + "branchName" => "cry-2-new-thing", + "description" => "Some description", + "assignee" => nil, + "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"} + } + } + } + }} + ]) + + assert capture_io(fn -> + assert {:ok, %Issue{identifier: "CRY-2"}} = + Creation.make_da_issue!( + title: "New thing", + description: "Some description", + team: "ENG", + labels: ["urgent"], + project: "Manhattan Rollout" + ) + end) == "" + end + 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 9e05f72..6e3d872 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.{Issue, Team, User} + alias LinearCli.Linear.{Issue, Team} defp issue(attrs \\ %{}) do struct!( @@ -18,483 +18,6 @@ defmodule LinearCli.CLI.IssueHelpersTest do ) end - defp stub_responses(pairs) do - Req.Test.stub(LinearCli.Api, fn conn -> - {:ok, body, conn} = Plug.Conn.read_body(conn) - %{"query" => query} = Jason.decode!(body) - - case Enum.find(pairs, fn {match, _resp} -> String.contains?(query, match) end) do - {_match, response} -> Req.Test.json(conn, response) - nil -> raise "no stub matched query: #{query}" - end - end) - end - - defp issue_updated(overrides) do - issue_map = - Map.merge( - %{ - "id" => "i1", - "identifier" => "CRY-1", - "title" => "Fix the thing", - "branchName" => "cry-1-fix-the-thing", - "description" => "It is broken", - "assignee" => nil, - "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"}, - "comments" => %{"nodes" => []} - }, - overrides - ) - - %{"data" => %{"issueUpdate" => %{"issue" => issue_map}}} - end - - defp team_projects(projects) do - %{"data" => %{"team" => %{"projects" => %{"nodes" => projects}}}} - end - - describe "make_da_issue!/1 (Ruby: CLI::Issue#make_da_issue!)" do - test "creates the issue with resolved title/description/team/labels/project" do - stub_responses([ - {"team(id: $id)", - %{ - "data" => %{ - "team" => %{ - "id" => "t1", - "key" => "ENG", - "name" => "Engineering", - "description" => nil - } - } - }}, - {"issueLabels", - %{ - "data" => %{ - "issueLabels" => %{ - "edges" => [ - %{ - "node" => %{ - "id" => "l1", - "name" => "urgent", - "description" => nil, - "isGroup" => false - } - } - ] - } - } - }}, - {"projects(first: 100", - team_projects([ - %{ - "id" => "p1", - "name" => "Manhattan Rollout", - "content" => nil, - "slugId" => "abc", - "description" => nil, - "url" => "https://linear.app/x/project/manhattan-rollout-abc" - } - ])}, - {"issueCreate", - %{ - "data" => %{ - "issueCreate" => %{ - "issue" => %{ - "id" => "i2", - "identifier" => "CRY-2", - "title" => "New thing", - "branchName" => "cry-2-new-thing", - "description" => "Some description", - "assignee" => nil, - "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"} - } - } - } - }} - ]) - - assert capture_io(fn -> - assert {:ok, %Issue{identifier: "CRY-2"}} = - IssueHelpers.make_da_issue!( - title: "New thing", - description: "Some description", - team: "ENG", - labels: ["urgent"], - project: "Manhattan Rollout" - ) - end) == "" - end - end - - describe "gimme_da_issue!/2 (Ruby: CLI::Issue#gimme_da_issue!)" do - test "when already assigned to the caller, says so and doesn't reassign" do - stub_responses([ - {"issue(id: $id)", - %{ - "data" => %{ - "issue" => %{ - "id" => "i1", - "identifier" => "CRY-1", - "title" => "Fix the thing", - "branchName" => "cry-1-fix-the-thing", - "description" => nil, - "assignee" => %{ - "id" => "u1", - "name" => "Ada", - "email" => "ada@x.com", - "teams" => %{"nodes" => []} - }, - "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"}, - "comments" => %{"nodes" => []} - } - } - }} - ]) - - me = %User{id: "u1", name: "Ada", email: "ada@x.com"} - - assert capture_io(fn -> - assert {:ok, %Issue{identifier: "CRY-1"}} = - IssueHelpers.gimme_da_issue!("CRY-1", me: me) - end) =~ "You are already assigned CRY-1" - end - - test "when unassigned, self-assigns" do - stub_responses([ - {"issue(id: $id)", - %{ - "data" => %{ - "issue" => %{ - "id" => "i1", - "identifier" => "CRY-1", - "title" => "Fix the thing", - "branchName" => "cry-1-fix-the-thing", - "description" => nil, - "assignee" => nil, - "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"}, - "comments" => %{"nodes" => []} - } - } - }}, - {"issueUpdate", - issue_updated(%{ - "assignee" => %{ - "id" => "u1", - "name" => "Ada", - "email" => "ada@x.com", - "teams" => %{"nodes" => []} - } - })} - ]) - - me = %User{id: "u1", name: "Ada", email: "ada@x.com"} - - output = - capture_io(fn -> - assert {:ok, %Issue{assignee: %User{id: "u1"}}} = - IssueHelpers.gimme_da_issue!("CRY-1", me: me) - end) - - assert output =~ "Assigning issue CRY-1 to ya" - end - - test "when already assigned to someone else, self-assigns" do - stub_responses([ - {"issue(id: $id)", - %{ - "data" => %{ - "issue" => %{ - "id" => "i1", - "identifier" => "CRY-1", - "title" => "Fix the thing", - "branchName" => "cry-1-fix-the-thing", - "description" => nil, - "assignee" => %{ - "id" => "u2", - "name" => "Bob", - "email" => "bob@x.com", - "teams" => %{"nodes" => []} - }, - "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"}, - "comments" => %{"nodes" => []} - } - } - }}, - {"issueUpdate", - issue_updated(%{ - "assignee" => %{ - "id" => "u1", - "name" => "Ada", - "email" => "ada@x.com", - "teams" => %{"nodes" => []} - } - })} - ]) - - me = %User{id: "u1", name: "Ada", email: "ada@x.com"} - - assert capture_io(fn -> - assert {:ok, %Issue{assignee: %User{id: "u1"}}} = - IssueHelpers.gimme_da_issue!("CRY-1", me: me) - end) =~ "Assigning issue CRY-1 to ya" - end - - test "with status: opt, resolves state per team and sends stateId" do - stub_responses([ - {"issue(id: $id)", - %{ - "data" => %{ - "issue" => %{ - "id" => "i1", - "identifier" => "CRY-1", - "title" => "Fix the thing", - "branchName" => "cry-1-fix-the-thing", - "description" => nil, - "assignee" => nil, - "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"}, - "comments" => %{"nodes" => []} - } - } - }}, - {"states {", - %{ - "data" => %{ - "team" => %{ - "states" => %{ - "nodes" => [ - %{ - "id" => "s1", - "name" => "In Progress", - "position" => 1.0, - "type" => "started", - "description" => nil - } - ] - } - } - } - }}, - {"issueUpdate", - issue_updated(%{ - "assignee" => %{ - "id" => "u1", - "name" => "Ada", - "email" => "ada@x.com", - "teams" => %{"nodes" => []} - } - })} - ]) - - me = %User{id: "u1", name: "Ada", email: "ada@x.com"} - - capture_io(fn -> - assert {:ok, %Issue{identifier: "CRY-1"}} = - IssueHelpers.gimme_da_issue!("CRY-1", me: me, status: "In Progress") - end) - end - - test "with status: opt, case-insensitive match sends correct stateId" do - test_pid = self() - - Req.Test.stub(LinearCli.Api, fn conn -> - {:ok, body, conn} = Plug.Conn.read_body(conn) - decoded = Jason.decode!(body) - %{"query" => query} = decoded - - cond do - String.contains?(query, "issue(id: $id)") -> - Req.Test.json(conn, %{ - "data" => %{ - "issue" => %{ - "id" => "i1", - "identifier" => "CRY-1", - "title" => "Fix the thing", - "branchName" => "cry-1-fix-the-thing", - "description" => nil, - "assignee" => nil, - "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"}, - "comments" => %{"nodes" => []} - } - } - }) - - String.contains?(query, "states {") -> - Req.Test.json(conn, %{ - "data" => %{ - "team" => %{ - "states" => %{ - "nodes" => [ - %{ - "id" => "s99", - "name" => "Todo", - "position" => 0.0, - "type" => "unstarted", - "description" => nil - } - ] - } - } - } - }) - - String.contains?(query, "issueUpdate") -> - send(test_pid, {:input, decoded["variables"]["input"]}) - - Req.Test.json( - conn, - issue_updated(%{ - "assignee" => %{ - "id" => "u1", - "name" => "Ada", - "email" => "ada@x.com", - "teams" => %{"nodes" => []} - } - }) - ) - - true -> - raise "no stub matched: #{query}" - end - end) - - me = %User{id: "u1", name: "Ada", email: "ada@x.com"} - - capture_io(fn -> - assert {:ok, _} = IssueHelpers.gimme_da_issue!("CRY-1", me: me, status: "todo") - end) - - assert_received {:input, input} - assert input["stateId"] == "s99" - end - - test "with status: opt, already-assigned still sends stateId mutation" do - test_pid = self() - - Req.Test.stub(LinearCli.Api, fn conn -> - {:ok, body, conn} = Plug.Conn.read_body(conn) - decoded = Jason.decode!(body) - %{"query" => query} = decoded - - cond do - String.contains?(query, "issue(id: $id)") -> - Req.Test.json(conn, %{ - "data" => %{ - "issue" => %{ - "id" => "i1", - "identifier" => "CRY-1", - "title" => "Fix the thing", - "branchName" => "cry-1-fix-the-thing", - "description" => nil, - "assignee" => %{ - "id" => "u1", - "name" => "Ada", - "email" => "ada@x.com", - "teams" => %{"nodes" => []} - }, - "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"}, - "comments" => %{"nodes" => []} - } - } - }) - - String.contains?(query, "states {") -> - Req.Test.json(conn, %{ - "data" => %{ - "team" => %{ - "states" => %{ - "nodes" => [ - %{ - "id" => "s2", - "name" => "In Progress", - "position" => 1.0, - "type" => "started", - "description" => nil - } - ] - } - } - } - }) - - String.contains?(query, "issueUpdate") -> - send(test_pid, {:input, decoded["variables"]["input"]}) - - Req.Test.json( - conn, - issue_updated(%{ - "assignee" => %{ - "id" => "u1", - "name" => "Ada", - "email" => "ada@x.com", - "teams" => %{"nodes" => []} - } - }) - ) - - true -> - raise "no stub matched: #{query}" - end - end) - - me = %User{id: "u1", name: "Ada", email: "ada@x.com"} - - capture_io(fn -> - assert {:ok, _} = IssueHelpers.gimme_da_issue!("CRY-1", me: me, status: "In Progress") - end) - - assert_received {:input, input} - assert input["assigneeId"] == "u1" - assert input["stateId"] == "s2" - end - - test "with status: opt, unknown name returns smells_bad error" do - stub_responses([ - {"issue(id: $id)", - %{ - "data" => %{ - "issue" => %{ - "id" => "i1", - "identifier" => "CRY-1", - "title" => "Fix the thing", - "branchName" => "cry-1-fix-the-thing", - "description" => nil, - "assignee" => nil, - "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"}, - "comments" => %{"nodes" => []} - } - } - }}, - {"states {", - %{ - "data" => %{ - "team" => %{ - "states" => %{ - "nodes" => [ - %{ - "id" => "s1", - "name" => "Todo", - "position" => 0.0, - "type" => "unstarted", - "description" => nil - } - ] - } - } - } - }} - ]) - - me = %User{id: "u1", name: "Ada", email: "ada@x.com"} - - capture_io(fn -> - assert {:error, {:smells_bad, msg}} = - IssueHelpers.gimme_da_issue!("CRY-1", me: me, status: "NoSuch") - - assert msg =~ "Unknown status" - end) - end - end - describe "create_pr!/3 and issue_pr/2" do test "create_pr!/3 forwards to the injectable runner" do runner = fn title, body -> "ran with #{title}/#{body}" end diff --git a/app/test/linear_cli/cli/profile_defaults_test.exs b/app/test/linear_cli/cli/profile_defaults_test.exs index 6358d42..9117ddf 100644 --- a/app/test/linear_cli/cli/profile_defaults_test.exs +++ b/app/test/linear_cli/cli/profile_defaults_test.exs @@ -7,7 +7,8 @@ defmodule LinearCli.CLI.ProfileDefaultsTest do use ExUnit.Case, async: false import ExUnit.CaptureIO - alias LinearCli.CLI.{Commands, IssueHelpers} + alias LinearCli.CLI.Commands + alias LinearCli.CLI.Issue.Creation alias LinearCli.Linear.User alias LinearCli.Profiles @@ -522,7 +523,7 @@ defmodule LinearCli.CLI.ProfileDefaultsTest do end end - describe "IssueHelpers.make_da_issue!/1 falls back to the active profile" do + describe "Creation.make_da_issue!/1 falls back to the active profile" do test "uses the active profile's team/project when both are omitted from opts" do {:ok, _} = Profiles.create("manhattan", team: "ENG", project: "Manhattan Rollout") :ok = Profiles.activate("manhattan") @@ -562,7 +563,7 @@ defmodule LinearCli.CLI.ProfileDefaultsTest do assert capture_io(fn -> assert {:ok, %{identifier: "CRY-2"}} = - IssueHelpers.make_da_issue!( + Creation.make_da_issue!( title: "New thing", description: "Some description", labels: ["urgent"] @@ -610,7 +611,7 @@ defmodule LinearCli.CLI.ProfileDefaultsTest do end) capture_io(fn -> - IssueHelpers.make_da_issue!( + Creation.make_da_issue!( title: "New thing", description: "Some description", labels: ["urgent"],