From aa4657f80ee02772bd2e2af7b15210b64f9dfeb9 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Sun, 16 Aug 2026 09:41:05 -0400 Subject: [PATCH 1/2] fix(issue-list): add --no-profile flag to bypass active profile defaults Adds a `--no-profile` boolean flag to `issue list`. When set, the `team_key` and `project_id` fields no longer fall back to `Profiles.default_team()` / `Profiles.default_project()`, so the command behaves as if no profile is active. Explicit `--team` / `--project` still take effect even when `--no-profile` is set. Co-Authored-By: Claude Sonnet 4.6 --- app/lib/linear_cli/cli.ex | 4 + app/lib/linear_cli/cli/commands.ex | 8 +- .../linear_cli/cli/issue_commands_test.exs | 27 ++++ .../linear_cli/cli/profile_defaults_test.exs | 139 ++++++++++++++++++ 4 files changed, 176 insertions(+), 2 deletions(-) diff --git a/app/lib/linear_cli/cli.ex b/app/lib/linear_cli/cli.ex index 7c637a2..dc9cb2d 100644 --- a/app/lib/linear_cli/cli.ex +++ b/app/lib/linear_cli/cli.ex @@ -509,6 +509,10 @@ defmodule LinearCli.CLI do long: "--no-mine", help: "List the most recent issues, not just your own" ], + no_profile: [ + long: "--no-profile", + help: "Ignore the active profile's team/project defaults" + ], full: [short: "-f", long: "--full", help: "Show full issue details"], all: [ long: "--all", diff --git a/app/lib/linear_cli/cli/commands.ex b/app/lib/linear_cli/cli/commands.ex index 97d752f..d676022 100644 --- a/app/lib/linear_cli/cli/commands.ex +++ b/app/lib/linear_cli/cli/commands.ex @@ -226,9 +226,13 @@ defmodule LinearCli.CLI.Commands do passed explicitly always win over the active profile. """ def issue_list(%{flags: flags, options: options, unknown: ids}) do - team_key = options.team || Profiles.default_team() + no_profile = Map.get(flags, :no_profile, false) + team_key = options.team || (unless no_profile, do: Profiles.default_team()) - with {:ok, project_id} <- resolve_project_id(options.project || Profiles.default_project()) do + project_source = + options.project || (unless no_profile, do: Profiles.default_project()) + + with {:ok, project_id} <- resolve_project_id(project_source) do input = %{ ids: Enum.map(ids, &IssueHelpers.expand_issue_id/1), mine: !flags.no_mine, diff --git a/app/test/linear_cli/cli/issue_commands_test.exs b/app/test/linear_cli/cli/issue_commands_test.exs index a5510a0..dfa6892 100644 --- a/app/test/linear_cli/cli/issue_commands_test.exs +++ b/app/test/linear_cli/cli/issue_commands_test.exs @@ -282,6 +282,33 @@ defmodule LinearCli.CLI.IssueCommandsTest do assert Map.has_key?(filter, "canceledAt") end + test "--no-profile bypasses active profile defaults via the full CLI dispatch path" do + test_pid = self() + + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + decoded = Jason.decode!(body) + query = decoded["query"] + + if String.contains?(query, "projects(") do + raise "--no-profile must not query projects when --project wasn't given" + end + + send(test_pid, {:filter, decoded["variables"]["filter"]}) + Req.Test.json(conn, issues_response([issue_map()])) + end) + + output = + capture_io(fn -> + assert :ok = LinearCli.CLI.main(["issue", "list", "--no-profile"]) + end) + + assert output =~ "CRY-1" + assert_received {:filter, filter} + refute Map.has_key?(filter, "team") + refute Map.has_key?(filter, "project") + end + test "--status with an unknown type exits 1 (Optimus parse error)" do test_pid = self() halt = fn code -> send(test_pid, {:halted, code}) end diff --git a/app/test/linear_cli/cli/profile_defaults_test.exs b/app/test/linear_cli/cli/profile_defaults_test.exs index 5205f13..78c282f 100644 --- a/app/test/linear_cli/cli/profile_defaults_test.exs +++ b/app/test/linear_cli/cli/profile_defaults_test.exs @@ -216,6 +216,74 @@ defmodule LinearCli.CLI.ProfileDefaultsTest do assert filter["project"] == %{"id" => %{"eq" => "p2"}} end + test "--no-profile bypasses the active profile's team/project defaults" do + {:ok, _} = Profiles.create("manhattan", team: "CRY", project: "Manhattan Rollout") + :ok = Profiles.activate("manhattan") + + test_pid = self() + + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + decoded = Jason.decode!(body) + query = decoded["query"] + + cond do + String.contains?(query, "issues(filter") -> + send(test_pid, {:filter, decoded["variables"]["filter"]}) + Req.Test.json(conn, issues_response([issue_map()])) + + true -> + raise "no stub matched query: #{query}" + end + end) + + result = %{ + flags: %{no_mine: false, unassigned: false, full: false, no_profile: true}, + options: %{team: nil, project: nil, output: "text"}, + unknown: [] + } + + capture_io(fn -> assert :ok = Commands.issue_list(result) end) + + assert_received {:filter, filter} + refute Map.has_key?(filter, "team") + refute Map.has_key?(filter, "project") + end + + test "--no-profile with an explicit --team still applies the explicit team" do + {:ok, _} = Profiles.create("manhattan", team: "CRY", project: "Manhattan Rollout") + :ok = Profiles.activate("manhattan") + + test_pid = self() + + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + decoded = Jason.decode!(body) + query = decoded["query"] + + cond do + String.contains?(query, "issues(filter") -> + send(test_pid, {:filter, decoded["variables"]["filter"]}) + Req.Test.json(conn, issues_response([issue_map()])) + + true -> + raise "no stub matched query: #{query}" + end + end) + + result = %{ + flags: %{no_mine: false, unassigned: false, full: false, no_profile: true}, + options: %{team: "ENG", project: nil, output: "text"}, + unknown: [] + } + + capture_io(fn -> assert :ok = Commands.issue_list(result) end) + + assert_received {:filter, filter} + assert filter["team"] == %{"key" => %{"eq" => "ENG"}} + refute Map.has_key?(filter, "project") + end + test "resolves bare issue numbers (positional ids) via the active profile's team" do {:ok, _} = Profiles.create("manhattan", team: "CRY") :ok = Profiles.activate("manhattan") @@ -250,6 +318,77 @@ defmodule LinearCli.CLI.ProfileDefaultsTest do end end + describe "Commands.issue_list/1 with --no-profile bypasses active profile defaults" do + test "ignores both team and project defaults when --no-profile is set" do + {:ok, _} = Profiles.create("manhattan", team: "CRY", project: "Manhattan Rollout") + :ok = Profiles.activate("manhattan") + + test_pid = self() + + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + decoded = Jason.decode!(body) + query = decoded["query"] + + if String.contains?(query, "projects(first: $first") do + raise "--no-profile must not query projects when --project wasn't given" + end + + if String.contains?(query, "issues(filter") do + send(test_pid, {:filter, decoded["variables"]["filter"]}) + Req.Test.json(conn, issues_response([issue_map()])) + else + raise "no stub matched query: #{query}" + end + end) + + result = %{ + flags: %{no_mine: false, unassigned: false, full: false, no_profile: true}, + options: %{team: nil, project: nil, output: "text"}, + unknown: [] + } + + output = capture_io(fn -> assert :ok = Commands.issue_list(result) end) + + assert output =~ "CRY-1" + assert_received {:filter, filter} + refute Map.has_key?(filter, "team") + refute Map.has_key?(filter, "project") + end + + test "--no-profile with explicit --team still applies the explicit team" do + {:ok, _} = Profiles.create("manhattan", team: "CRY", project: "Manhattan Rollout") + :ok = Profiles.activate("manhattan") + + test_pid = self() + + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + decoded = Jason.decode!(body) + query = decoded["query"] + + if String.contains?(query, "issues(filter") do + send(test_pid, {:filter, decoded["variables"]["filter"]}) + Req.Test.json(conn, issues_response([issue_map()])) + else + raise "no stub matched query: #{query}" + end + end) + + result = %{ + flags: %{no_mine: false, unassigned: false, full: false, no_profile: true}, + options: %{team: "ENG", project: nil, output: "text"}, + unknown: [] + } + + capture_io(fn -> assert :ok = Commands.issue_list(result) end) + + assert_received {:filter, filter} + assert filter["team"] == %{"key" => %{"eq" => "ENG"}} + refute Map.has_key?(filter, "project") + end + end + describe "Commands.issue_update/1 resolves bare issue numbers via the active profile" do test "expands a bare positional id before looking it up" do {:ok, _} = Profiles.create("manhattan", team: "CRY") From 03ff373b4a1defa6aa8349889e83245df88c16eb Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Sun, 16 Aug 2026 09:54:15 -0400 Subject: [PATCH 2/2] fix(issue-list): address code-review feedback on --no-profile flag - Remove unnecessary parentheses from `unless` expressions in `Commands.issue_list/1` to pass `mix format --check-formatted` - Consolidate duplicate `--no-profile` describe block in `profile_defaults_test.exs` into the existing "falls back to the active profile" describe; strengthen the bypass test with a project- query guard and output assertion - Add missing test: `--no-profile` with explicit `--project` still applies the explicit project filter while ignoring profile defaults --- app/lib/linear_cli/cli/commands.ex | 4 +- .../linear_cli/cli/profile_defaults_test.exs | 103 ++++++------------ 2 files changed, 38 insertions(+), 69 deletions(-) diff --git a/app/lib/linear_cli/cli/commands.ex b/app/lib/linear_cli/cli/commands.ex index d676022..9f51bd2 100644 --- a/app/lib/linear_cli/cli/commands.ex +++ b/app/lib/linear_cli/cli/commands.ex @@ -227,10 +227,10 @@ defmodule LinearCli.CLI.Commands do """ def issue_list(%{flags: flags, options: options, unknown: ids}) do no_profile = Map.get(flags, :no_profile, false) - team_key = options.team || (unless no_profile, do: Profiles.default_team()) + team_key = options.team || unless no_profile, do: Profiles.default_team() project_source = - options.project || (unless no_profile, do: Profiles.default_project()) + options.project || unless no_profile, do: Profiles.default_project() with {:ok, project_id} <- resolve_project_id(project_source) do input = %{ diff --git a/app/test/linear_cli/cli/profile_defaults_test.exs b/app/test/linear_cli/cli/profile_defaults_test.exs index 78c282f..09f0a55 100644 --- a/app/test/linear_cli/cli/profile_defaults_test.exs +++ b/app/test/linear_cli/cli/profile_defaults_test.exs @@ -227,13 +227,15 @@ defmodule LinearCli.CLI.ProfileDefaultsTest do decoded = Jason.decode!(body) query = decoded["query"] - cond do - String.contains?(query, "issues(filter") -> - send(test_pid, {:filter, decoded["variables"]["filter"]}) - Req.Test.json(conn, issues_response([issue_map()])) + if String.contains?(query, "projects(first: $first") do + raise "--no-profile must not query projects when --project wasn't given" + end - true -> - raise "no stub matched query: #{query}" + if String.contains?(query, "issues(filter") do + send(test_pid, {:filter, decoded["variables"]["filter"]}) + Req.Test.json(conn, issues_response([issue_map()])) + else + raise "no stub matched query: #{query}" end end) @@ -243,8 +245,9 @@ defmodule LinearCli.CLI.ProfileDefaultsTest do unknown: [] } - capture_io(fn -> assert :ok = Commands.issue_list(result) end) + output = capture_io(fn -> assert :ok = Commands.issue_list(result) end) + assert output =~ "CRY-1" assert_received {:filter, filter} refute Map.has_key?(filter, "team") refute Map.has_key?(filter, "project") @@ -284,8 +287,8 @@ defmodule LinearCli.CLI.ProfileDefaultsTest do refute Map.has_key?(filter, "project") end - test "resolves bare issue numbers (positional ids) via the active profile's team" do - {:ok, _} = Profiles.create("manhattan", team: "CRY") + test "--no-profile with an explicit --project still applies the explicit project" do + {:ok, _} = Profiles.create("manhattan", team: "CRY", project: "Manhattan Rollout") :ok = Profiles.activate("manhattan") test_pid = self() @@ -296,68 +299,33 @@ defmodule LinearCli.CLI.ProfileDefaultsTest do query = decoded["query"] cond do - String.contains?(query, "issue(id: $id)") -> - send(test_pid, {:id, decoded["variables"]["id"]}) - Req.Test.json(conn, %{"data" => %{"issue" => issue_map()}}) + String.contains?(query, "projects(first: $first") -> + Req.Test.json(conn, all_projects([project_map("p3", "Platform Cleanup")])) + + String.contains?(query, "issues(filter") -> + send(test_pid, {:filter, decoded["variables"]["filter"]}) + Req.Test.json(conn, issues_response([issue_map()])) true -> raise "no stub matched query: #{query}" end end) - result = %{ - flags: %{no_mine: false, unassigned: false, full: false}, - options: %{team: nil, project: nil, output: "text"}, - unknown: ["42"] - } - - output = capture_io(fn -> assert :ok = Commands.issue_list(result) end) - - assert output =~ "CRY-1" - assert_received {:id, "CRY-42"} - end - end - - describe "Commands.issue_list/1 with --no-profile bypasses active profile defaults" do - test "ignores both team and project defaults when --no-profile is set" do - {:ok, _} = Profiles.create("manhattan", team: "CRY", project: "Manhattan Rollout") - :ok = Profiles.activate("manhattan") - - test_pid = self() - - Req.Test.stub(LinearCli.Api, fn conn -> - {:ok, body, conn} = Plug.Conn.read_body(conn) - decoded = Jason.decode!(body) - query = decoded["query"] - - if String.contains?(query, "projects(first: $first") do - raise "--no-profile must not query projects when --project wasn't given" - end - - if String.contains?(query, "issues(filter") do - send(test_pid, {:filter, decoded["variables"]["filter"]}) - Req.Test.json(conn, issues_response([issue_map()])) - else - raise "no stub matched query: #{query}" - end - end) - result = %{ flags: %{no_mine: false, unassigned: false, full: false, no_profile: true}, - options: %{team: nil, project: nil, output: "text"}, + options: %{team: nil, project: "Platform Cleanup", output: "text"}, unknown: [] } - output = capture_io(fn -> assert :ok = Commands.issue_list(result) end) + capture_io(fn -> assert :ok = Commands.issue_list(result) end) - assert output =~ "CRY-1" assert_received {:filter, filter} refute Map.has_key?(filter, "team") - refute Map.has_key?(filter, "project") + assert filter["project"] == %{"id" => %{"eq" => "p3"}} end - test "--no-profile with explicit --team still applies the explicit team" do - {:ok, _} = Profiles.create("manhattan", team: "CRY", project: "Manhattan Rollout") + test "resolves bare issue numbers (positional ids) via the active profile's team" do + {:ok, _} = Profiles.create("manhattan", team: "CRY") :ok = Profiles.activate("manhattan") test_pid = self() @@ -367,25 +335,26 @@ defmodule LinearCli.CLI.ProfileDefaultsTest do decoded = Jason.decode!(body) query = decoded["query"] - if String.contains?(query, "issues(filter") do - send(test_pid, {:filter, decoded["variables"]["filter"]}) - Req.Test.json(conn, issues_response([issue_map()])) - else - raise "no stub matched query: #{query}" + cond do + String.contains?(query, "issue(id: $id)") -> + send(test_pid, {:id, decoded["variables"]["id"]}) + Req.Test.json(conn, %{"data" => %{"issue" => issue_map()}}) + + true -> + raise "no stub matched query: #{query}" end end) result = %{ - flags: %{no_mine: false, unassigned: false, full: false, no_profile: true}, - options: %{team: "ENG", project: nil, output: "text"}, - unknown: [] + flags: %{no_mine: false, unassigned: false, full: false}, + options: %{team: nil, project: nil, output: "text"}, + unknown: ["42"] } - capture_io(fn -> assert :ok = Commands.issue_list(result) end) + output = capture_io(fn -> assert :ok = Commands.issue_list(result) end) - assert_received {:filter, filter} - assert filter["team"] == %{"key" => %{"eq" => "ENG"}} - refute Map.has_key?(filter, "project") + assert output =~ "CRY-1" + assert_received {:id, "CRY-42"} end end