From 2cf85845f12ca95d20dd6dba1ec9c52721252f82 Mon Sep 17 00:00:00 2001 From: bougyman's bot Date: Mon, 14 Sep 2026 13:28:42 -0400 Subject: [PATCH 1/2] feat(linear): expose issue priority and timestamps in JSON output Add priority (priority, priority_label, priority_sort_order) and timestamps (created_at, updated_at) to Issue and Comment resources so they are included in --output json for all commands. - Extend @issue_fields GraphQL selection with priority fields - Add five new attributes to LinearCli.Linear.Issue - Extract new fields in Issue.from_map/1 - Add created_at/updated_at to LinearCli.Linear.Comment (already in GraphQL selection via base_fields/0, but discarded until now) - Extract timestamps in Comment.from_map/1 - Update ash-domain-erd.adoc per maintenance contract Co-Authored-By: Claude Sonnet 4.6 --- app/lib/linear_cli/linear/comment.ex | 6 +++++- app/lib/linear_cli/linear/issue.ex | 12 +++++++++++- documents/ash-domain-erd.adoc | 12 ++++++++++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/lib/linear_cli/linear/comment.ex b/app/lib/linear_cli/linear/comment.ex index 16ddc46..7a1d851 100644 --- a/app/lib/linear_cli/linear/comment.ex +++ b/app/lib/linear_cli/linear/comment.ex @@ -24,6 +24,8 @@ defmodule LinearCli.Linear.Comment do attribute :body, :string, public?: true attribute :url, :string, public?: true attribute :user, :term, public?: true + attribute :created_at, :string, public?: true + attribute :updated_at, :string, public?: true end @doc "GraphQL field selection for a comment's own fields (Ruby: Comment::Base)." @@ -37,7 +39,9 @@ defmodule LinearCli.Linear.Comment do id: map["id"], body: map["body"], url: map["url"], - user: map["user"] && LinearCli.Linear.User.from_map(map["user"]) + user: map["user"] && LinearCli.Linear.User.from_map(map["user"]), + created_at: map["createdAt"], + updated_at: map["updatedAt"] ) end end diff --git a/app/lib/linear_cli/linear/issue.ex b/app/lib/linear_cli/linear/issue.ex index 36bd9f2..fc7dbfb 100644 --- a/app/lib/linear_cli/linear/issue.ex +++ b/app/lib/linear_cli/linear/issue.ex @@ -71,6 +71,11 @@ defmodule LinearCli.Linear.Issue do attribute :branch_name, :string, public?: true attribute :description, :string, public?: true attribute :url, :string, public?: true + attribute :priority, :float, public?: true + attribute :priority_label, :string, public?: true + attribute :priority_sort_order, :float, public?: true + attribute :created_at, :string, public?: true + attribute :updated_at, :string, public?: true attribute :assignee, :term, public?: true attribute :state, :term, public?: true attribute :team, :term, public?: true @@ -80,7 +85,7 @@ defmodule LinearCli.Linear.Issue do attribute :inverse_relations, {:array, :term}, public?: true, default: [] end - @issue_fields "id identifier title branchName description url createdAt updatedAt" + @issue_fields "id identifier title branchName description url priority priorityLabel prioritySortOrder createdAt updatedAt" @state_fields "id name type" @doc "GraphQL field selection for an issue plus its assignee/team (Ruby: Issue.base_fragment)." @@ -120,6 +125,11 @@ defmodule LinearCli.Linear.Issue do branch_name: map["branchName"], description: map["description"], url: map["url"], + priority: map["priority"], + priority_label: map["priorityLabel"], + priority_sort_order: map["prioritySortOrder"], + created_at: map["createdAt"], + updated_at: map["updatedAt"], assignee: map["assignee"] && LinearCli.Linear.User.from_map(map["assignee"]), state: map["state"] && LinearCli.Linear.WorkflowState.from_map(map["state"]), team: map["team"] && LinearCli.Linear.Team.from_map(map["team"]), diff --git a/documents/ash-domain-erd.adoc b/documents/ash-domain-erd.adoc index 94a318c..72dd7d1 100644 --- a/documents/ash-domain-erd.adoc +++ b/documents/ash-domain-erd.adoc @@ -68,6 +68,12 @@ erDiagram string title string branch_name string description + string url + float priority + string priority_label + float priority_sort_order + string created_at + string updated_at User assignee WorkflowState state Team team @@ -101,6 +107,8 @@ erDiagram string body string url User user + string created_at + string updated_at } ProjectUpdate { string id PK @@ -153,7 +161,7 @@ Nine resources are registered in `LinearCli.Linear` | `LinearCli.Linear.Issue` | `id` (`:string`) -| `identifier`, `title`, `branch_name`, `description`, `assignee` (`:term`), `state` (`:term`), `team` (`:term`), `comments` (`{:array, :term}`), `labels` (`{:array, :term}`), `relations` (`{:array, :term}`), `inverse_relations` (`{:array, :term}`) +| `identifier`, `title`, `branch_name`, `description`, `url`, `priority` (`:float`), `priority_label` (`:string`), `priority_sort_order` (`:float`), `created_at` (`:string`, ISO-8601), `updated_at` (`:string`, ISO-8601), `assignee` (`:term`), `state` (`:term`), `team` (`:term`), `comments` (`{:array, :term}`), `labels` (`{:array, :term}`), `relations` (`{:array, :term}`), `inverse_relations` (`{:array, :term}`) | `LinearCli.Linear.Label` | `id` (`:string`) @@ -165,7 +173,7 @@ Nine resources are registered in `LinearCli.Linear` | `LinearCli.Linear.Comment` | `id` (`:string`) -| `body`, `url`, `user` (`:term`) +| `body`, `url`, `user` (`:term`), `created_at` (`:string`, ISO-8601), `updated_at` (`:string`, ISO-8601) | `LinearCli.Linear.ProjectUpdate` | `id` (`:string`) From 82b3959f3a30a2e92e853e9ea9432397a53eadad Mon Sep 17 00:00:00 2001 From: bougyman's bot Date: Mon, 14 Sep 2026 13:28:55 -0400 Subject: [PATCH 2/2] test(linear): add priority and timestamp coverage for Issue and Comment - Update issue_map/1 and comment_created/0 test fixtures with priority and timestamp fields matching the new GraphQL response shape - Add from_map/1 round-trip tests in issue_test and comment_test - Add --output json snapshot tests for issue list and issue view - Add Display.show/2 JSON tests for priority=0 preservation and snake_case field name stability Co-Authored-By: Claude Sonnet 4.6 --- .../cli/commands/issues/read_test.exs | 130 ++++++++++++ app/test/linear_cli/cli/display_test.exs | 86 ++++++++ app/test/linear_cli/linear/comment_test.exs | 39 ++++ app/test/linear_cli/linear/issue_test.exs | 189 ++++++++++++++++++ app/test/support/issue_commands_helpers.ex | 17 +- 5 files changed, 460 insertions(+), 1 deletion(-) diff --git a/app/test/linear_cli/cli/commands/issues/read_test.exs b/app/test/linear_cli/cli/commands/issues/read_test.exs index 318bf4d..44be2a5 100644 --- a/app/test/linear_cli/cli/commands/issues/read_test.exs +++ b/app/test/linear_cli/cli/commands/issues/read_test.exs @@ -820,6 +820,38 @@ defmodule LinearCli.CLI.Commands.Issues.ReadTest do assert [label] = decoded["labels"] assert label["name"] == "Bug" end + + test "--output json includes priority and timestamps in issue list" do + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"query" => _} = Jason.decode!(body) + + Req.Test.json( + conn, + issues_response([ + issue_map(%{ + "priority" => 0.0, + "priorityLabel" => "No priority", + "prioritySortOrder" => 0.0, + "createdAt" => "2024-01-15T10:30:00.000Z", + "updatedAt" => "2024-01-16T12:00:00.000Z" + }) + ]) + ) + end) + + output = + capture_io(fn -> + assert :ok = LinearCli.CLI.main(["issue", "list", "--output", "json"]) + end) + + assert {:ok, [decoded]} = Jason.decode(output) + assert decoded["priority"] === 0.0 + refute is_nil(decoded["priority"]) + assert decoded["priority_label"] == "No priority" + assert decoded["created_at"] == "2024-01-15T10:30:00.000Z" + assert decoded["updated_at"] == "2024-01-16T12:00:00.000Z" + end end describe "issue view" do @@ -865,6 +897,104 @@ defmodule LinearCli.CLI.Commands.Issues.ReadTest do assert decoded["title"] == "Fix the thing" end + test "--output json includes priority=0 as a real value and timestamps" do + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"query" => _} = Jason.decode!(body) + + Req.Test.json(conn, %{ + "data" => %{ + "issue" => + issue_map(%{ + "priority" => 0.0, + "priorityLabel" => "No priority", + "prioritySortOrder" => 0.0, + "createdAt" => "2024-01-15T10:30:00.000Z", + "updatedAt" => "2024-01-16T12:00:00.000Z" + }) + } + }) + end) + + output = + capture_io(fn -> + assert :ok = LinearCli.CLI.main(["issue", "view", "CRY-1", "--output", "json"]) + end) + + decoded = Jason.decode!(output) + assert decoded["priority"] === 0.0 + refute is_nil(decoded["priority"]) + assert decoded["priority_label"] == "No priority" + assert decoded["priority_sort_order"] === 0.0 + assert decoded["created_at"] == "2024-01-15T10:30:00.000Z" + assert decoded["updated_at"] == "2024-01-16T12:00:00.000Z" + end + + test "--output json includes non-zero priority and timestamps" do + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"query" => _} = Jason.decode!(body) + + Req.Test.json(conn, %{ + "data" => %{ + "issue" => + issue_map(%{ + "priority" => 2.0, + "priorityLabel" => "High", + "prioritySortOrder" => 100.5, + "createdAt" => "2024-03-01T09:00:00.000Z", + "updatedAt" => "2024-03-02T11:00:00.000Z" + }) + } + }) + end) + + output = + capture_io(fn -> + assert :ok = LinearCli.CLI.main(["issue", "view", "CRY-1", "--output", "json"]) + end) + + decoded = Jason.decode!(output) + assert decoded["priority"] == 2.0 + assert decoded["priority_label"] == "High" + assert decoded["priority_sort_order"] == 100.5 + assert decoded["created_at"] == "2024-03-01T09:00:00.000Z" + assert decoded["updated_at"] == "2024-03-02T11:00:00.000Z" + end + + test "--output json includes timestamps on nested comments" do + comment_map = %{ + "id" => "c1", + "body" => "First comment", + "url" => "https://linear.app/team/issue/CRY-1#comment-c1", + "user" => nil, + "createdAt" => "2024-01-20T08:00:00.000Z", + "updatedAt" => "2024-01-20T08:30:00.000Z" + } + + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"query" => _} = Jason.decode!(body) + + Req.Test.json(conn, %{ + "data" => %{ + "issue" => issue_map(%{"comments" => %{"nodes" => [comment_map]}}) + } + }) + end) + + output = + capture_io(fn -> + assert :ok = LinearCli.CLI.main(["issue", "view", "CRY-1", "--output", "json"]) + end) + + decoded = Jason.decode!(output) + assert [comment] = decoded["comments"] + assert comment["id"] == "c1" + assert comment["created_at"] == "2024-01-20T08:00:00.000Z" + assert comment["updated_at"] == "2024-01-20T08:30:00.000Z" + end + test "lc i v ISSUE_ID alias routes to issue view" do Req.Test.stub(LinearCli.Api, fn conn -> {:ok, body, conn} = Plug.Conn.read_body(conn) diff --git a/app/test/linear_cli/cli/display_test.exs b/app/test/linear_cli/cli/display_test.exs index 38c0580..b00bb9b 100644 --- a/app/test/linear_cli/cli/display_test.exs +++ b/app/test/linear_cli/cli/display_test.exs @@ -258,4 +258,90 @@ defmodule LinearCli.CLI.DisplayTest do assert output =~ "EXT-6" refute output =~ "[" end + + test "JSON output preserves priority=0 as a real numeric value" do + issue = %Issue{ + id: "issue-7", + identifier: "EXT-7", + title: "No priority issue", + description: nil, + priority: 0.0, + priority_label: "No priority", + priority_sort_order: 0.0, + comments: [], + labels: [] + } + + output = capture_io(fn -> Display.show(issue, %{output: "json"}) end) + + decoded = Jason.decode!(output) + assert decoded["priority"] === 0.0 + refute is_nil(decoded["priority"]) + assert decoded["priority_label"] == "No priority" + assert decoded["priority_sort_order"] === 0.0 + end + + test "JSON output uses snake_case field names for priority and timestamps" do + issue = %Issue{ + id: "issue-8", + identifier: "EXT-8", + title: "Field name stability", + description: nil, + priority: 1.0, + priority_label: "Urgent", + priority_sort_order: 42.0, + created_at: "2024-01-15T10:30:00.000Z", + updated_at: "2024-01-16T12:00:00.000Z", + comments: [], + labels: [] + } + + output = capture_io(fn -> Display.show(issue, %{output: "json"}) end) + + decoded = Jason.decode!(output) + assert Map.has_key?(decoded, "priority") + assert Map.has_key?(decoded, "priority_label") + assert Map.has_key?(decoded, "priority_sort_order") + assert Map.has_key?(decoded, "created_at") + assert Map.has_key?(decoded, "updated_at") + refute Map.has_key?(decoded, "priorityLabel") + refute Map.has_key?(decoded, "prioritySortOrder") + refute Map.has_key?(decoded, "createdAt") + refute Map.has_key?(decoded, "updatedAt") + assert decoded["priority"] == 1.0 + assert decoded["priority_label"] == "Urgent" + assert decoded["created_at"] == "2024-01-15T10:30:00.000Z" + assert decoded["updated_at"] == "2024-01-16T12:00:00.000Z" + end + + test "JSON output includes timestamps on nested comment structs" do + alias LinearCli.Linear.Comment + + issue = %Issue{ + id: "issue-9", + identifier: "EXT-9", + title: "With comments", + description: nil, + priority: nil, + comments: [ + %Comment{ + id: "c1", + body: "A comment", + url: "https://linear.app/x#comment-c1", + user: nil, + created_at: "2024-01-20T08:00:00.000Z", + updated_at: "2024-01-20T08:30:00.000Z" + } + ], + labels: [] + } + + output = capture_io(fn -> Display.show(issue, %{output: "json"}) end) + + decoded = Jason.decode!(output) + assert [comment] = decoded["comments"] + assert comment["id"] == "c1" + assert comment["created_at"] == "2024-01-20T08:00:00.000Z" + assert comment["updated_at"] == "2024-01-20T08:30:00.000Z" + end end diff --git a/app/test/linear_cli/linear/comment_test.exs b/app/test/linear_cli/linear/comment_test.exs index 728079c..88dc74b 100644 --- a/app/test/linear_cli/linear/comment_test.exs +++ b/app/test/linear_cli/linear/comment_test.exs @@ -44,5 +44,44 @@ defmodule LinearCli.Linear.CommentTest do assert {:error, %Ash.Error.Unknown{}} = Linear.add_comment("nope", "Looks good") end + + test "extracts createdAt and updatedAt from the API response" do + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"variables" => %{"issueId" => _, "body" => _}} = Jason.decode!(body) + + Req.Test.json(conn, %{ + "data" => %{ + "commentCreate" => %{ + "comment" => %{ + "id" => "c1", + "body" => "Hello", + "url" => "https://linear.app/team/issue/CRY-1#comment-c1", + "user" => nil, + "createdAt" => "2024-03-10T14:00:00.000Z", + "updatedAt" => "2024-03-10T14:05:00.000Z" + } + } + } + }) + end) + + assert {:ok, comment} = Linear.add_comment("CRY-1", "Hello") + assert comment.created_at == "2024-03-10T14:00:00.000Z" + assert comment.updated_at == "2024-03-10T14:05:00.000Z" + end + + test "tolerates missing timestamp fields (nil stays nil)" do + comment = + LinearCli.Linear.Comment.from_map(%{ + "id" => "c2", + "body" => "Old comment", + "url" => nil, + "user" => nil + }) + + assert is_nil(comment.created_at) + assert is_nil(comment.updated_at) + end end end diff --git a/app/test/linear_cli/linear/issue_test.exs b/app/test/linear_cli/linear/issue_test.exs index 0cc30e0..9164dff 100644 --- a/app/test/linear_cli/linear/issue_test.exs +++ b/app/test/linear_cli/linear/issue_test.exs @@ -678,6 +678,195 @@ defmodule LinearCli.Linear.IssueTest do end end + describe "from_map/1 priority and timestamp extraction" do + test "extracts priority=0 as a real float value (not nil)" do + map = %{ + "id" => "i1", + "identifier" => "CRY-1", + "title" => "No priority issue", + "branchName" => "cry-1-no-priority", + "description" => nil, + "priority" => 0.0, + "priorityLabel" => "No priority", + "prioritySortOrder" => 0.0, + "createdAt" => "2024-01-15T10:30:00.000Z", + "updatedAt" => "2024-01-16T12:00:00.000Z", + "assignee" => nil, + "state" => nil, + "team" => nil + } + + issue = LinearCli.Linear.Issue.from_map(map) + + assert issue.priority === 0.0 + refute is_nil(issue.priority) + assert issue.priority_label == "No priority" + assert issue.priority_sort_order == 0.0 + assert issue.created_at == "2024-01-15T10:30:00.000Z" + assert issue.updated_at == "2024-01-16T12:00:00.000Z" + end + + test "extracts non-zero priority values" do + map = %{ + "id" => "i2", + "identifier" => "CRY-2", + "title" => "Urgent issue", + "branchName" => "cry-2-urgent", + "description" => nil, + "priority" => 1.0, + "priorityLabel" => "Urgent", + "prioritySortOrder" => 42.5, + "createdAt" => "2024-02-01T08:00:00.000Z", + "updatedAt" => "2024-02-02T09:00:00.000Z", + "assignee" => nil, + "state" => nil, + "team" => nil + } + + issue = LinearCli.Linear.Issue.from_map(map) + + assert issue.priority == 1.0 + assert issue.priority_label == "Urgent" + assert issue.priority_sort_order == 42.5 + assert issue.created_at == "2024-02-01T08:00:00.000Z" + assert issue.updated_at == "2024-02-02T09:00:00.000Z" + end + + test "tolerates missing priority fields (nil stays nil)" do + map = %{ + "id" => "i3", + "identifier" => "CRY-3", + "title" => "Legacy issue", + "branchName" => "cry-3-legacy", + "description" => nil, + "assignee" => nil, + "state" => nil, + "team" => nil + } + + issue = LinearCli.Linear.Issue.from_map(map) + + assert is_nil(issue.priority) + assert is_nil(issue.priority_label) + assert is_nil(issue.priority_sort_order) + assert is_nil(issue.created_at) + assert is_nil(issue.updated_at) + end + + test "GraphQL query selection includes priority fields" do + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"query" => query} = Jason.decode!(body) + + assert query =~ "priority" + assert query =~ "priorityLabel" + assert query =~ "prioritySortOrder" + + Req.Test.json(conn, %{ + "data" => %{"issues" => %{"edges" => [], "pageInfo" => %{"hasNextPage" => false}}} + }) + end) + + assert {:ok, []} = Linear.issues(%{labels: [], mine: false}) + end + end + + describe "Issue.from_map/1 priority and timestamp fields" do + test "extracts priority, priority_label, priority_sort_order, created_at, updated_at" do + Req.Test.stub(LinearCli.Api, fn conn -> + Req.Test.json(conn, %{ + "data" => %{ + "issues" => %{ + "edges" => [ + %{ + "node" => %{ + "id" => "i1", + "identifier" => "CRY-1", + "title" => "Fix it", + "branchName" => "cry-1-fix-it", + "description" => nil, + "priority" => 2.0, + "priorityLabel" => "High", + "prioritySortOrder" => 14.5, + "createdAt" => "2024-01-15T10:30:00.000Z", + "updatedAt" => "2024-01-16T12:00:00.000Z", + "assignee" => nil, + "state" => nil, + "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"} + }, + "cursor" => "c1" + } + ], + "pageInfo" => %{"hasNextPage" => false} + } + } + }) + end) + + assert {:ok, [issue]} = Linear.issues(%{mine: false}) + assert issue.priority == 2.0 + assert issue.priority_label == "High" + assert issue.priority_sort_order == 14.5 + assert issue.created_at == "2024-01-15T10:30:00.000Z" + assert issue.updated_at == "2024-01-16T12:00:00.000Z" + end + + test "priority 0 is preserved as a real value, not treated as missing" do + Req.Test.stub(LinearCli.Api, fn conn -> + Req.Test.json(conn, %{ + "data" => %{ + "issues" => %{ + "edges" => [ + %{ + "node" => %{ + "id" => "i1", + "identifier" => "CRY-1", + "title" => "Fix it", + "branchName" => "cry-1", + "description" => nil, + "priority" => 0.0, + "priorityLabel" => "No priority", + "prioritySortOrder" => 0.0, + "createdAt" => "2024-01-15T10:30:00.000Z", + "updatedAt" => "2024-01-15T10:30:00.000Z", + "assignee" => nil, + "state" => nil, + "team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"} + }, + "cursor" => "c1" + } + ], + "pageInfo" => %{"hasNextPage" => false} + } + } + }) + end) + + assert {:ok, [issue]} = Linear.issues(%{mine: false}) + assert issue.priority == 0.0 + refute is_nil(issue.priority) + end + + test "GraphQL query includes priority and timestamp fields" do + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + %{"query" => query} = Jason.decode!(body) + + assert query =~ "priority" + assert query =~ "priorityLabel" + assert query =~ "prioritySortOrder" + assert query =~ "createdAt" + assert query =~ "updatedAt" + + Req.Test.json(conn, %{ + "data" => %{"issues" => %{"edges" => [], "pageInfo" => %{"hasNextPage" => false}}} + }) + end) + + assert {:ok, []} = Linear.issues(%{mine: false}) + end + end + describe "issues/1 include_labels field selection" do test "include_labels: true, labels: [] requests label fields without adding a label filter" do Req.Test.stub(LinearCli.Api, fn conn -> diff --git a/app/test/support/issue_commands_helpers.ex b/app/test/support/issue_commands_helpers.ex index e22cdcf..3b0e089 100644 --- a/app/test/support/issue_commands_helpers.ex +++ b/app/test/support/issue_commands_helpers.ex @@ -66,6 +66,11 @@ defmodule LinearCli.CLI.IssueCommandsHelpers do "title" => "Fix the thing", "branchName" => "cry-1-fix-the-thing", "description" => "It is broken", + "priority" => 0.0, + "priorityLabel" => "No priority", + "prioritySortOrder" => 0.0, + "createdAt" => "2024-01-15T10:30:00.000Z", + "updatedAt" => "2024-01-16T12:00:00.000Z", "assignee" => nil, "state" => %{"id" => "s1", "name" => "In Progress", "type" => "started"}, "team" => team_map(), @@ -81,7 +86,17 @@ defmodule LinearCli.CLI.IssueCommandsHelpers do def comment_created do %{ - "data" => %{"commentCreate" => %{"comment" => %{"id" => "c1", "body" => "x", "url" => "u"}}} + "data" => %{ + "commentCreate" => %{ + "comment" => %{ + "id" => "c1", + "body" => "x", + "url" => "u", + "createdAt" => "2024-01-15T10:30:00.000Z", + "updatedAt" => "2024-01-15T10:30:00.000Z" + } + } + } } end