diff --git a/CLAUDE.md b/CLAUDE.md index 7fc1baf..e78e169 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/crates/lineark/src/commands/usage.rs b/crates/lineark/src/commands/usage.rs index a6e0cad..65c789f 100644 --- a/crates/lineark/src/commands/usage.rs +++ b/crates/lineark/src/commands/usage.rs @@ -29,45 +29,19 @@ COMMANDS: lineark whoami Show authenticated user lineark teams list List all teams lineark teams read Full team detail (members, settings) - lineark teams create 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 Update a team - [--name NAME] [--description TEXT] ... (same flags as create, all optional) - lineark teams delete Delete a team - lineark teams members add --user NAME Add member to team - lineark teams members remove --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 Full project detail (lead, members, status, dates, teams) - lineark projects create --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 --team KEY ... Create project (--help for flags) lineark labels list [--team KEY] List labels (group, team, parent, color) - lineark labels create 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 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 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 [--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 Full issue detail incl. sub-issues, comments, relations lineark issues find-branch Find issue by Git branch name @@ -76,7 +50,7 @@ COMMANDS: [--status NAME,...] [--show-done] Comma-separated status names lineark issues create --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 @@ -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 diff --git a/crates/lineark/tests/offline.rs b/crates/lineark/tests/offline.rs index 68fe480..8cf412d 100644 --- a/crates/lineark/tests/offline.rs +++ b/crates/lineark/tests/offline.rs @@ -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")); } @@ -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] @@ -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] @@ -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 ───────────────────────────────────────────────────── @@ -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 ────────────────────────────────────────────────────── @@ -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) ───────────────────────── @@ -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 ─────────────────────────────────────────────────────── @@ -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 ───────────────────────────────────────────────────────── @@ -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 ────────────────────────────────────── @@ -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 ──────────────────────────────────────────── diff --git a/crates/lineark/tests/online.rs b/crates/lineark/tests/online.rs index 03420ea..91d399e 100644 --- a/crates/lineark/tests/online.rs +++ b/crates/lineark/tests/online.rs @@ -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. @@ -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() @@ -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",