Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ and best practices for agents to follow.
- Phase 11 Plan: documents/phase-11-plan.adoc
- Phase 12 Plan: documents/phase-12-plan.adoc
- Phase 13 Plan: documents/phase-13-plan.adoc
- Phase 14 Plan: documents/phase-14-plan.adoc
- Burrito distribution decision: documents/burrito-decision.adoc

## Project Structure

Expand Down Expand Up @@ -57,7 +59,7 @@ Supporting directories (not Mix projects):
`lproj`). Each one just calls `exec lc ...`.
- `ci/` — Shell scripts for CI and release: conventional-commit enforcement,
container build/publish, Homebrew formula bump.
- `githooks/` — `commit-msg` and `pre-push` hooks; installed by `mix setup`.
- `git-hooks/` — `commit-msg` and `pre-push` hooks; installed by `mix setup`.
- `oci/` — `Containerfile` for the published container image.
- `schema/` — The Linear GraphQL schema (`LinearAPI.graphql`), kept for reference.
- `cinemas/` — Terminal session recordings (`.cinema.gif`) embedded in Readme.adoc.
Expand Down Expand Up @@ -127,7 +129,7 @@ not need a structural-doc update.
## Standards

- Conventional Commits: app/usage-rules.md — enforced by the `commit-msg`
and `pre-push` hooks at `githooks/` (run `mix setup` once per clone to
and `pre-push` hooks at `git-hooks/` (run `mix setup` once per clone to
activate them).
- Dogfooding & running `lc` locally (no MCP, no escript): app/usage-rules.md
- Accessibility: app/usage-rules.md — the actual reason this project
Expand Down
8 changes: 7 additions & 1 deletion app/test/linear_cli/api_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule LinearCli.ApiTest do
# not per-process) LINEAR_API_KEY env var. test_helper.exs sets a default
# for the rest of the suite; running this module concurrently with it would race.
use ExUnit.Case, async: false
import ExUnit.CaptureLog

test "returns {:ok, data} on a successful response" do
Req.Test.stub(LinearCli.Api, fn conn ->
Expand Down Expand Up @@ -33,7 +34,12 @@ defmodule LinearCli.ApiTest do
})
end)

assert LinearCli.Api.call("{ issue(id: $id) { id } }") == {:ok, %{"issue" => nil}}
log =
capture_log(fn ->
assert LinearCli.Api.call("{ issue(id: $id) { id } }") == {:ok, %{"issue" => nil}}
end)

assert log =~ "Linear API partial-success: 1 field error(s) discarded, data returned"
end

test "returns {:error, {:unexpected_response, body}} when there's neither data nor errors" do
Expand Down
28 changes: 13 additions & 15 deletions app/test/linear_cli/cli/issue_commands_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -439,20 +439,20 @@ defmodule LinearCli.CLI.IssueCommandsTest do
end

test "--state with an unknown type exits 1 (Optimus parse error)" do
test_pid = self()
halt = fn code -> send(test_pid, {:halted, code}) end

# Optimus catches the bad value and calls halt.(1); with a fake halt that
# doesn't terminate the process, execution continues and eventually crashes
# (same artifact as the --help test in cli_test.exs). Rescue it so the test
# can still verify halt was called with the right code.
try do
LinearCli.CLI.main(["issue", "list", "--state", "badtype"], halt)
rescue
_ -> :ok
end
# The production halt function never returns. Throw from the test double
# too, so the parser's error path stops before it reaches the normal CLI
# dispatch and emits an unrelated exception diagnostic.
output =
capture_io(fn ->
assert catch_throw(
LinearCli.CLI.main(
["issue", "list", "--state", "badtype"],
fn code -> throw({:halted, code}) end
)
) == {:halted, 1}
end)

assert_received {:halted, 1}
assert output =~ "invalid value \"badtype\" for --state option"
end

test "--labels filters by a single label name (case-insensitive)" do
Expand Down Expand Up @@ -1043,8 +1043,6 @@ defmodule LinearCli.CLI.IssueCommandsTest do
end

test "-y/--yes with all required flags creates and self-assigns without any prompts" do
me = %User{id: "u1", name: "Ada", email: "ada@x.com"}

created_issue =
issue_map(%{
"id" => "i2",
Expand Down
6 changes: 5 additions & 1 deletion app/test/linear_cli/cli_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule LinearCli.CLITest do
use ExUnit.Case, async: true
import ExUnit.CaptureIO
import ExUnit.CaptureLog

setup do
Req.Test.stub(LinearCli.Api, fn conn ->
Expand Down Expand Up @@ -558,10 +559,13 @@ defmodule LinearCli.CLITest do

output =
capture_io(:stderr, fn ->
LinearCli.CLI.main(["issue", "develop", "CRY-999"], halt)
log = capture_log(fn -> LinearCli.CLI.main(["issue", "develop", "CRY-999"], halt) end)
send(test_pid, {:log, log})
end)

assert_received {:halted, 66}
assert_received {:log, log}
assert log =~ "Linear API partial-success: 1 field error(s) discarded, data returned"
assert output =~ "No issue found with id"
refute output =~ "What the heck is this?"
end
Expand Down
33 changes: 16 additions & 17 deletions app/test/linear_cli/git_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ defmodule LinearCli.GitTest do
# under System.tmp_dir!(), never against the real project working
# directory. See house rule 6 in the project instructions.
setup do
origin_path = tmp_path("origin")
File.mkdir_p!(origin_path)
origin_path = tmp_dir!("origin")
{_output, 0} = System.cmd("git", ["init", "--bare", "-q"], cd: origin_path)

# `git init --bare`'s HEAD symref follows the runner's ambient
Expand All @@ -23,27 +22,29 @@ defmodule LinearCli.GitTest do
{_output, 0} =
System.cmd("git", ["symbolic-ref", "HEAD", "refs/heads/main"], cd: origin_path)

repo_path = tmp_path("repo")
File.mkdir_p!(repo_path)
repo_path = tmp_dir!("repo")
init_repo!(repo_path)
commit_file!(repo_path, "README.md", "hello")
{_output, 0} = System.cmd("git", ["branch", "-M", "main"], cd: repo_path)
{_output, 0} = System.cmd("git", ["remote", "add", "origin", origin_path], cd: repo_path)
{_output, 0} = System.cmd("git", ["push", "-q", "-u", "origin", "main"], cd: repo_path)

on_exit(fn ->
File.rm_rf!(origin_path)
File.rm_rf!(repo_path)
end)

%{repo: repo_path, origin: origin_path}
end

defp tmp_path(prefix) do
Path.join(
System.tmp_dir!(),
"linear_cli_git_test_#{prefix}_#{System.unique_integer([:positive, :monotonic])}"
)
# `System.unique_integer/1` is unique only within the current BEAM VM. A
# fresh `mix test` process starts its sequence over, so an interrupted prior
# run can otherwise reuse its stale /tmp fixture. A cryptographic nonce makes
# the directory unique across processes as well; register cleanup before any
# Git command can fail, so failed setup does not leave another collision
# behind.
defp tmp_dir!(prefix) do
nonce = :crypto.strong_rand_bytes(16) |> Base.url_encode64(padding: false)
path = Path.join(System.tmp_dir!(), "linear_cli_git_test_#{prefix}_#{nonce}")

File.mkdir!(path)
on_exit(fn -> File.rm_rf!(path) end)
path
end

defp init_repo!(path) do
Expand Down Expand Up @@ -113,11 +114,9 @@ defmodule LinearCli.GitTest do
end

test "returns an error tuple when there is no origin remote" do
repo_path = tmp_path("repo_no_origin")
File.mkdir_p!(repo_path)
repo_path = tmp_dir!("repo_no_origin")
init_repo!(repo_path)
commit_file!(repo_path, "README.md", "hello")
on_exit(fn -> File.rm_rf!(repo_path) end)

assert {:error, _reason} = Git.default_branch(cwd: repo_path)
end
Expand Down
10 changes: 8 additions & 2 deletions app/test/linear_cli/linear/issue_test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule LinearCli.Linear.IssueTest do
use ExUnit.Case, async: true
import ExUnit.CaptureLog

alias LinearCli.Linear

Expand Down Expand Up @@ -244,8 +245,13 @@ defmodule LinearCli.Linear.IssueTest do
})
end)

assert {:error, %Ash.Error.Unknown{errors: [%{value: [not_found: _id]}]}} =
Linear.issues(%{ids: ["nope"]})
log =
capture_log(fn ->
assert {:error, %Ash.Error.Unknown{errors: [%{value: [not_found: _id]}]}} =
Linear.issues(%{ids: ["nope"]})
end)

assert log =~ "Linear API partial-success: 1 field error(s) discarded, data returned"
end

describe "create_issue/3+" do
Expand Down
18 changes: 9 additions & 9 deletions app/usage-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
- Use the imperative, present tense in the description (`add`, not `added`/`adds`).
- Mark breaking changes with `!` before the colon (e.g. `feat!: ...`).
- Bare `Merge branch ...` subjects are rejected — reword as `chore: Merge branch ...`.
- Enforced locally by the `commit-msg` hook at `githooks/commit-msg` (each
commit's own subject, via `ci/validate_conventional_commit.sh`) and the
`pre-push` hook at `githooks/pre-push` (every commit about to be pushed,
via `ci/conventional_commits.sh` - catches anything that slipped past
`commit-msg`, e.g. a commit made before the hooks were installed) — run
`mix setup` once per clone to activate both.
- Enforced in CI across a whole PR's commit range by the same
`ci/conventional_commits.sh` the `pre-push` hook uses (skips GitHub's own
auto-generated update-branch merge commits).
- Enforced locally by the `commit-msg` hook at `git-hooks/commit-msg` (each
commit's own subject, via its shared subject validator) and the `pre-push`
hook at `git-hooks/pre-push` (`mix precommit`, which validates
every commit about to be pushed and runs the dependency security audits,
formatting, static analysis, and tests) — run `mix setup` once per clone
to activate both.
- Enforced in CI across a whole PR's commit range by
`ci/conventional_commits.sh` (skips GitHub's own auto-generated
update-branch merge commits).

## Dogfooding: use `lc`, not a Linear MCP server or skill

Expand Down
Loading