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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ make test-online # online tests (live API, serial, needs

**Online tests must run serially.** They hit the live Linear API, which has plan-level limits (e.g. max teams). Running them in parallel causes spurious failures from resource exhaustion. `make test-online` handles this automatically.

**Online tests must be self-contained.** Never assume resources (projects, issues, teams, etc.) already exist in the test workspace. Each test must create the resources it needs and clean them up afterwards using RAII guards (`TeamGuard`, `IssueGuard`, `ProjectGuard`). These guards auto-delete the resource on drop, ensuring cleanup even when the test panics. Use unique names for created resources (e.g. `format!("[test] my thing {}", &uuid::Uuid::new_v4().to_string()[..8])`) to avoid conflicts from zombie resources left by previously-failed runs. Use `retry_with_backoff` when querying recently-created resources, since the Linear API is eventually consistent. Always check `output.status.success()` and include stdout/stderr in assertion messages before parsing JSON output — a bare `.unwrap()` on JSON parsing hides the real error (e.g. auth failures).
**Online tests must be self-contained.** Never assume resources (projects, issues, teams, etc.) already exist in the test workspace. Each test must create the resources it needs and clean them up afterwards using RAII guards (`TeamGuard`, `IssueGuard`, `ProjectGuard`). These guards auto-delete the resource on drop, ensuring cleanup even when the test panics. Always use `create_test_team()` to create teams — it sets unique keys per team to avoid human identifier collisions in Linear's search index. Use unique names for other resources (e.g. `format!("[test] my thing {}", &uuid::Uuid::new_v4().to_string()[..8])`) to avoid conflicts from zombie resources left by previously-failed runs. Use `retry_with_backoff` when querying recently-created resources, since the Linear API is eventually consistent. Always check `output.status.success()` and include stdout/stderr in assertion messages before parsing JSON output — a bare `.unwrap()` on JSON parsing hides the real error (e.g. auth failures).

## Updating the schema

Expand Down
92 changes: 15 additions & 77 deletions crates/lineark/src/commands/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,45 +29,19 @@ COMMANDS:
lineark whoami Show authenticated user
lineark teams list List all teams
lineark teams read <KEY-OR-ID> Full team detail (members, settings)
lineark teams create <NAME> Create a team
[--key KEY] [--description TEXT] Key (auto-generated if omitted)
[--icon ICON] [--color COLOR] Icon, color
[--timezone TZ] [--private] Timezone, private flag
[--cycles-enabled] [--triage-enabled] Enable cycles/triage
lineark teams update <KEY-OR-ID> Update a team
[--name NAME] [--description TEXT] ... (same flags as create, all optional)
lineark teams delete <KEY-OR-ID> Delete a team
lineark teams members add <TEAM> --user NAME Add member to team
lineark teams members remove <TEAM> --user NAME Remove member from team
lineark teams create|update|delete ... Manage teams (--help for flags)
lineark teams members add|remove ... Manage membership (--help for flags)
lineark users list [--active] List users
lineark projects list [--led-by-me] List all projects (with lead)
lineark projects read <NAME-OR-ID> Full project detail (lead, members, status, dates, teams)
lineark projects create <NAME> --team KEY[,KEY] Create a new project
[--description TEXT] [--lead NAME-OR-ID|me] Description, project lead
[--members NAME,...|me] Project members (comma-separated)
[--start-date DATE] [--target-date DATE] Dates (YYYY-MM-DD)
[-p 0-4] [--content TEXT] Priority, markdown content
[--icon ICON] [--color COLOR] Icon, color
lineark projects create <NAME> --team KEY ... Create project (--help for flags)
lineark labels list [--team KEY] List labels (group, team, parent, color)
lineark labels create <NAME> Create a label (workspace-wide if no --team)
[--team KEY] [--color HEX] Team, color
[--description TEXT] Description
[--parent-label-group ID] Nest under a group label
[--make-label-group] Create as a group label
lineark labels update <ID> Update a label
[--name TEXT] [--color HEX] Name, color
[--description TEXT] Description
[--parent-label-group ID] Nest under a group label
[--clear-parent-label-group] Remove parent group
[--make-label-group] [--clear-label-group] Promote/demote group
lineark labels delete <ID> Delete a label
lineark labels create|update|delete ... Manage labels (--help for flags)
lineark cycles list [-l N] [--team KEY] List cycles
[--active] Only the active cycle
[--around-active N] Active ± N neighbors
[--active] [--around-active N] Active cycle / ± N neighbors
lineark cycles read <ID> [--team KEY] Read cycle (UUID, name, or number)
lineark issues list [-l N] [--team KEY] Active issues (done/canceled hidden), newest first
[--project NAME-OR-ID] Filter by project
[--mine] Only issues assigned to me
[--project NAME-OR-ID] [--mine] Filter by project, assignee
[--show-done] Include done/canceled issues
lineark issues read <IDENTIFIER> Full issue detail incl. sub-issues, comments, relations
lineark issues find-branch <BRANCH> Find issue by Git branch name
Expand All @@ -76,7 +50,7 @@ COMMANDS:
[--status NAME,...] [--show-done] Comma-separated status names
lineark issues create <TITLE> --team KEY Create an issue
[-p 0-4] [-e N] [--assignee NAME-OR-ID|me] 0=none 1=urgent 2=high 3=medium 4=low
[--labels NAME,...] [-d TEXT] [-s NAME] Label names (team-scoped), status name
[--labels NAME,...] [-d TEXT] [-s NAME] Label names, status name
[--parent ID] [--project NAME-OR-ID] Parent issue, project, cycle
[--cycle NAME-OR-ID]
lineark issues update <IDENTIFIER> Update an issue
Expand All @@ -86,54 +60,18 @@ COMMANDS:
[--clear-labels] [-t TEXT] [-d TEXT] Title, description
[--parent ID] [--clear-parent] Set or remove parent
[--project NAME-OR-ID] [--cycle NAME-OR-ID] Project, cycle
lineark issues batch-update ID [ID ...] Batch update multiple issues
[-s NAME] [-p 0-4] [--assignee NAME-OR-ID|me] Status, priority, assignee
lineark issues archive <IDENTIFIER> Archive an issue
lineark issues unarchive <IDENTIFIER> Unarchive a previously archived issue
lineark issues delete <IDENTIFIER> Delete (trash) an issue
[--permanently] Permanently delete instead of trashing
lineark issues batch-update ID [ID ...] Batch update (--help for flags)
lineark issues archive|unarchive|delete ... Lifecycle ops (--help for flags)
lineark comments create <ISSUE-ID> --body TEXT Comment on an issue
lineark comments update <COMMENT-UUID> Update a comment
--body TEXT New body in markdown
lineark comments resolve <COMMENT-UUID> Resolve a comment thread
[--resolving-comment UUID] Reply that resolves thread
lineark comments unresolve <COMMENT-UUID> Unresolve a comment thread
lineark comments delete <COMMENT-UUID> Delete a comment
lineark relations create <ISSUE> Create an issue relation
--blocks <ISSUE> Source blocks target
--blocked-by <ISSUE> Source is blocked by target
--related <ISSUE> Mark issues as related
--duplicate <ISSUE> Mark as duplicate
--similar <ISSUE> Mark as similar
lineark relations delete <RELATION-UUID> Delete an issue relation
lineark comments update|resolve|unresolve|delete Manage comments (--help for flags)
lineark relations create|delete ... Issue relations (--help for flags)
lineark documents list [--limit N] List documents (lean output)
[--project NAME-OR-ID] [--issue ID] Filter by project or issue
lineark documents read <ID> Read document (includes content)
lineark documents create --title TEXT Create a document
[--content TEXT] [--project NAME-OR-ID] Project name or UUID
[--issue ID]
lineark documents update <ID> Update a document
[--title TEXT] [--content TEXT]
lineark documents delete <ID> Delete (trash) a document
lineark project-milestones list --project NAME List milestones for a project
lineark project-milestones read <ID> Read a milestone (UUID or name with --project)
[--project NAME-OR-ID]
lineark project-milestones create <NAME> Create a milestone
--project NAME-OR-ID [--target-date DATE] DATE = YYYY-MM-DD
[--description TEXT]
lineark project-milestones update <ID> Update a milestone
[--project NAME-OR-ID] [--name TEXT]
[--target-date DATE] [--description TEXT]
lineark project-milestones delete <ID> Delete a milestone
[--project NAME-OR-ID]
lineark embeds upload <FILE> [--public] Upload file to Linear, returns asset URL
Embed as markdown [name](url) in issues,
comments, or documents
--public only works for images (not SVG)
lineark embeds download <URL> Download any file by URL (works with
[--output PATH] [--overwrite] Linear CDN URLs and external URLs alike)
lineark self update Update lineark to the latest release
lineark self update --check Check if an update is available
lineark documents create|update|delete ... Manage documents (--help for flags)
lineark project-milestones ... Milestones CRUD (--help for flags)
lineark embeds upload|download ... File embeds (--help for flags)
lineark self update [--check] Update lineark / check for updates

GLOBAL OPTIONS:
--api-token <TOKEN> Override API token
Expand Down
53 changes: 17 additions & 36 deletions crates/lineark/tests/offline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,9 @@ fn usage_includes_write_commands() {
.success()
.stdout(predicate::str::contains("issues create"))
.stdout(predicate::str::contains("issues update"))
.stdout(predicate::str::contains("issues archive"))
.stdout(predicate::str::contains("issues unarchive"))
.stdout(predicate::str::contains("issues delete"))
.stdout(predicate::str::contains("archive"))
.stdout(predicate::str::contains("unarchive"))
.stdout(predicate::str::contains("delete"))
.stdout(predicate::str::contains("comments create"));
}

Expand Down Expand Up @@ -288,9 +288,7 @@ fn usage_includes_documents_commands() {
.success()
.stdout(predicate::str::contains("documents list"))
.stdout(predicate::str::contains("documents read"))
.stdout(predicate::str::contains("documents create"))
.stdout(predicate::str::contains("documents update"))
.stdout(predicate::str::contains("documents delete"));
.stdout(predicate::str::contains("documents create|update|delete"));
}

#[test]
Expand All @@ -299,8 +297,7 @@ fn usage_includes_embeds_commands() {
.arg("usage")
.assert()
.success()
.stdout(predicate::str::contains("embeds download"))
.stdout(predicate::str::contains("embeds upload"));
.stdout(predicate::str::contains("embeds upload|download"));
}

#[test]
Expand Down Expand Up @@ -385,9 +382,7 @@ fn usage_includes_labels_crud() {
.assert()
.success()
.stdout(predicate::str::contains("labels list"))
.stdout(predicate::str::contains("labels create"))
.stdout(predicate::str::contains("labels update"))
.stdout(predicate::str::contains("labels delete"));
.stdout(predicate::str::contains("labels create|update|delete"));
}

// ── Auth error handling ─────────────────────────────────────────────────────
Expand Down Expand Up @@ -590,11 +585,7 @@ fn usage_includes_milestones_commands() {
.arg("usage")
.assert()
.success()
.stdout(predicate::str::contains("project-milestones list"))
.stdout(predicate::str::contains("project-milestones read"))
.stdout(predicate::str::contains("project-milestones create"))
.stdout(predicate::str::contains("project-milestones update"))
.stdout(predicate::str::contains("project-milestones delete"));
.stdout(predicate::str::contains("project-milestones"));
}

// ── Issues: new flags ──────────────────────────────────────────────────────
Expand Down Expand Up @@ -737,11 +728,13 @@ fn usage_includes_led_by_me() {

#[test]
fn usage_includes_members_flag() {
// --members is now behind --help (collapsed), but "members" still appears
// in the teams members line.
lineark()
.arg("usage")
.assert()
.success()
.stdout(predicate::str::contains("--members"));
.stdout(predicate::str::contains("members"));
}

// ── Cycle number parsing rejects NaN/inf (issue #6) ─────────────────────────
Expand Down Expand Up @@ -868,12 +861,9 @@ fn usage_includes_teams_create() {
.arg("usage")
.assert()
.success()
.stdout(predicate::str::contains("teams create"))
.stdout(predicate::str::contains("teams update"))
.stdout(predicate::str::contains("teams delete"))
.stdout(predicate::str::contains("teams create|update|delete"))
.stdout(predicate::str::contains("teams read"))
.stdout(predicate::str::contains("teams members add"))
.stdout(predicate::str::contains("teams members remove"));
.stdout(predicate::str::contains("teams members add|remove"));
}

// ── Issues find-branch ───────────────────────────────────────────────────────
Expand Down Expand Up @@ -1008,8 +998,7 @@ fn usage_includes_self_update_commands() {
.arg("usage")
.assert()
.success()
.stdout(predicate::str::contains("self update"))
.stdout(predicate::str::contains("--check"));
.stdout(predicate::str::contains("self update"));
}

// ── Comments delete ─────────────────────────────────────────────────────────
Expand Down Expand Up @@ -1038,7 +1027,9 @@ fn usage_includes_comments_delete() {
.arg("usage")
.assert()
.success()
.stdout(predicate::str::contains("comments delete"));
.stdout(predicate::str::contains(
"comments update|resolve|unresolve|delete",
));
}

// ── Comments update/resolve/unresolve ──────────────────────────────────────
Expand Down Expand Up @@ -1120,17 +1111,7 @@ fn usage_includes_comments_update() {
.arg("usage")
.assert()
.success()
.stdout(predicate::str::contains("comments update"));
}

#[test]
fn usage_includes_comments_resolve() {
lineark()
.arg("usage")
.assert()
.success()
.stdout(predicate::str::contains("comments resolve"))
.stdout(predicate::str::contains("comments unresolve"));
.stdout(predicate::str::contains("update|resolve|unresolve|delete"));
}

// ── Issues list --project filter ────────────────────────────────────────────
Expand Down
12 changes: 9 additions & 3 deletions crates/lineark/tests/online.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ where
/// Wait for the Linear API to propagate recently created resources.
/// Linear is eventually consistent — created resources may not be queryable immediately.
fn settle() {
std::thread::sleep(std::time::Duration::from_secs(2));
std::thread::sleep(std::time::Duration::from_secs(5));
}

/// Run a lineark CLI command with retry logic.
Expand Down Expand Up @@ -189,9 +189,15 @@ fn create_test_team() -> (String, String, TeamGuard) {
let client = Client::from_token(token.clone()).unwrap();
let rt = tokio::runtime::Runtime::new().unwrap();
let team: Team = rt.block_on(async {
let unique = format!("[test] cli {}", &uuid::Uuid::new_v4().to_string()[..8]);
let suffix = &uuid::Uuid::new_v4().to_string()[..8];
let unique = format!("[test] cli {suffix}");
// Use a unique key so issue identifiers (e.g. T1A2B3C4-1) don't collide
// across test runs. Linear's search index gets confused when many teams
// reuse the same auto-generated key "TES".
let key = format!("T{}", &suffix[..5]).to_uppercase();
let input = TeamCreateInput {
name: Some(unique),
key: Some(key),
..Default::default()
};
client.team_create::<Team>(None, input).await.unwrap()
Expand Down Expand Up @@ -1145,7 +1151,7 @@ mod online {
//
// Linear's search index is async — the newly created+archived issue may
// not be searchable immediately. Retry with generous backoff to avoid flakiness.
let stdout = retry_with_backoff(10, || {
let stdout = retry_with_backoff(12, || {
let output = lineark()
.args([
"--api-token",
Expand Down
Loading