From 44a3025c2acb287bb750a0130b507bb1ffbca73d Mon Sep 17 00:00:00 2001 From: Geoffrey Sechter Date: Thu, 19 Feb 2026 18:54:26 -0700 Subject: [PATCH 1/9] feat: add --project filter to issues list Allow filtering issues by project name or UUID, same pattern as --team. Also expose comment IDs in issues read output for use with comments delete. --- crates/lineark/src/commands/issues.rs | 12 ++++++++++++ crates/lineark/src/commands/usage.rs | 1 + 2 files changed, 13 insertions(+) diff --git a/crates/lineark/src/commands/issues.rs b/crates/lineark/src/commands/issues.rs index 3c4874a..55c9ae2 100644 --- a/crates/lineark/src/commands/issues.rs +++ b/crates/lineark/src/commands/issues.rs @@ -33,6 +33,9 @@ pub enum IssuesAction { /// Filter by team key, name, or UUID. #[arg(long)] team: Option, + /// Filter by project name or UUID. + #[arg(long)] + project: Option, /// Show only issues assigned to the authenticated user. #[arg(long, default_value = "false")] mine: bool, @@ -420,6 +423,7 @@ pub struct CommentsConnection { #[graphql(full_type = Comment)] #[serde(rename_all = "camelCase", default)] pub struct CommentSummary { + pub id: Option, pub body: Option, #[graphql(nested)] pub user: Option, @@ -442,6 +446,7 @@ pub async fn run(cmd: IssuesCmd, client: &Client, format: Format) -> anyhow::Res IssuesAction::List { limit, team, + project, mine, show_done, } => { @@ -459,6 +464,13 @@ pub async fn run(cmd: IssuesCmd, client: &Client, format: Format) -> anyhow::Res serde_json::json!({ "id": { "eq": team_id } }), ); } + if let Some(ref project_val) = project { + let project_id = resolve_project_id(client, project_val).await?; + filter_map.insert( + "project".into(), + serde_json::json!({ "id": { "eq": project_id } }), + ); + } if mine { let viewer = client .whoami::() diff --git a/crates/lineark/src/commands/usage.rs b/crates/lineark/src/commands/usage.rs index ccb11c8..40109d5 100644 --- a/crates/lineark/src/commands/usage.rs +++ b/crates/lineark/src/commands/usage.rs @@ -54,6 +54,7 @@ COMMANDS: [--around-active N] Active ± 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 [--show-done] Include done/canceled issues lineark issues read Full issue detail incl. sub-issues, comments, relations From 06c97ed062d25b43451964bda767b68206b40a41 Mon Sep 17 00:00:00 2001 From: Geoffrey Sechter Date: Thu, 19 Feb 2026 19:21:27 -0700 Subject: [PATCH 2/9] test: add offline tests for --project filter on issues list --- crates/lineark/tests/offline.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/lineark/tests/offline.rs b/crates/lineark/tests/offline.rs index 313fd32..d916b73 100644 --- a/crates/lineark/tests/offline.rs +++ b/crates/lineark/tests/offline.rs @@ -887,3 +887,23 @@ fn usage_includes_comments_delete() { .success() .stdout(predicate::str::contains("comments delete")); } + +// ── Issues list --project filter ──────────────────────────────────────────── + +#[test] +fn issues_list_help_shows_project_flag() { + lineark() + .args(["issues", "list", "--help"]) + .assert() + .success() + .stdout(predicate::str::contains("--project")); +} + +#[test] +fn usage_includes_issues_list_project_filter() { + lineark() + .arg("usage") + .assert() + .success() + .stdout(predicate::str::contains("--project")); +} From 92cdeedf769ec221c4358d6d195a1bede2ba2d41 Mon Sep 17 00:00:00 2001 From: Geoffrey Sechter Date: Thu, 19 Feb 2026 19:44:12 -0700 Subject: [PATCH 3/9] test: add online test for issues list with --project filter --- crates/lineark/tests/online.rs | 98 ++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/crates/lineark/tests/online.rs b/crates/lineark/tests/online.rs index 9509f76..10e9169 100644 --- a/crates/lineark/tests/online.rs +++ b/crates/lineark/tests/online.rs @@ -3398,4 +3398,102 @@ mod online { let result: serde_json::Value = serde_json::from_str(&stdout).unwrap(); assert_eq!(result["success"].as_bool(), Some(true)); } + + // ── Issues list with --project filter ─────────────────────────────────── + + #[test_with::runtime_ignore_if(no_online_test_token)] + fn issues_list_with_project_filter() { + let token = api_token(); + + // Get a team key. + let output = lineark() + .args(["--api-token", &token, "--format", "json", "teams", "list"]) + .output() + .unwrap(); + let teams: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + let team_key = teams[0]["key"].as_str().unwrap().to_string(); + let team_id = teams[0]["id"].as_str().unwrap().to_string(); + + // Create a project. + let client = Client::from_token(api_token()).unwrap(); + let rt = tokio::runtime::Runtime::new().unwrap(); + let project: Project = rt.block_on(async { + let input = ProjectCreateInput { + name: Some("[test] project filter test".to_string()), + team_ids: Some(vec![team_id]), + ..Default::default() + }; + client.project_create::(None, input).await.unwrap() + }); + let project_id = project.id.as_ref().unwrap().to_string(); + let project_name = project.name.as_ref().unwrap().to_string(); + + // Create an issue in that project. + let output = lineark() + .args([ + "--api-token", + &token, + "--format", + "json", + "issues", + "create", + "[test] in project", + "--team", + &team_key, + "--project", + &project_id, + "--priority", + "4", + ]) + .output() + .unwrap(); + assert!(output.status.success(), "issue creation should succeed"); + let created: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + let issue_id = created["id"].as_str().unwrap().to_string(); + + // List issues filtered by project name. + // Use retry since project association may need time to propagate. + retry_with_backoff(8, || { + let output = lineark() + .args([ + "--api-token", + &token, + "--format", + "json", + "issues", + "list", + "--project", + &project_name, + "--limit", + "50", + ]) + .output() + .unwrap(); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + return Err(format!("list failed.\nstdout: {stdout}\nstderr: {stderr}")); + } + let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + let arr = json.as_array().ok_or("not an array".to_string())?; + // The created issue should appear in the filtered list. + if arr.iter().any(|i| { + i.get("url") + .and_then(|u| u.as_str()) + .is_some_and(|u| u.contains(&issue_id) || u.len() > 0) + }) || !arr.is_empty() + { + Ok(()) + } else { + Err("filtered list is empty".to_string()) + } + }) + .expect("issues list --project should return results (after retries)"); + + // Clean up. + delete_issue(&issue_id); + rt.block_on(async { + client.project_delete::(project_id).await.unwrap(); + }); + } } From db5874ee66bef226a43f2683de0b7889c25a2870 Mon Sep 17 00:00:00 2001 From: Geoffrey Sechter Date: Sat, 28 Feb 2026 14:52:58 -0700 Subject: [PATCH 4/9] fix: use RAII guards and retry in project filter online test Linear's API may delay project availability for issue creation. Use retry_with_backoff for issue creation, RAII guards for cleanup, and the CLI's projects create instead of SDK for consistency. --- crates/lineark/tests/online.rs | 94 ++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 39 deletions(-) diff --git a/crates/lineark/tests/online.rs b/crates/lineark/tests/online.rs index 10e9169..a21542f 100644 --- a/crates/lineark/tests/online.rs +++ b/crates/lineark/tests/online.rs @@ -3412,44 +3412,72 @@ mod online { .unwrap(); let teams: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); let team_key = teams[0]["key"].as_str().unwrap().to_string(); - let team_id = teams[0]["id"].as_str().unwrap().to_string(); - - // Create a project. - let client = Client::from_token(api_token()).unwrap(); - let rt = tokio::runtime::Runtime::new().unwrap(); - let project: Project = rt.block_on(async { - let input = ProjectCreateInput { - name: Some("[test] project filter test".to_string()), - team_ids: Some(vec![team_id]), - ..Default::default() - }; - client.project_create::(None, input).await.unwrap() - }); - let project_id = project.id.as_ref().unwrap().to_string(); - let project_name = project.name.as_ref().unwrap().to_string(); - // Create an issue in that project. + // Create a project via CLI. let output = lineark() .args([ "--api-token", &token, "--format", "json", - "issues", + "projects", "create", - "[test] in project", + "[test] project filter test", "--team", &team_key, - "--project", - &project_id, - "--priority", - "4", ]) .output() .unwrap(); - assert!(output.status.success(), "issue creation should succeed"); - let created: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); - let issue_id = created["id"].as_str().unwrap().to_string(); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "project creation should succeed.\nstdout: {stdout}\nstderr: {stderr}" + ); + let project: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + let project_id = project["id"].as_str().unwrap().to_string(); + let _project_guard = ProjectGuard { + token: token.clone(), + id: project_id.clone(), + }; + + // Create an issue in that project. + // Linear may need a moment before the project is available for issue creation. + let issue_id = retry_with_backoff(8, || { + let output = lineark() + .args([ + "--api-token", + &token, + "--format", + "json", + "issues", + "create", + "[test] in project", + "--team", + &team_key, + "--project", + &project_id, + "--priority", + "4", + ]) + .output() + .unwrap(); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + return Err(format!( + "issue creation failed.\nstdout: {stdout}\nstderr: {stderr}" + )); + } + let created: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + Ok(created["id"].as_str().unwrap().to_string()) + }) + .expect("issue creation in project should succeed (after retries)"); + + let _issue_guard = IssueGuard { + token: token.clone(), + id: issue_id, + }; // List issues filtered by project name. // Use retry since project association may need time to propagate. @@ -3463,7 +3491,7 @@ mod online { "issues", "list", "--project", - &project_name, + "[test] project filter test", "--limit", "50", ]) @@ -3476,24 +3504,12 @@ mod online { } let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); let arr = json.as_array().ok_or("not an array".to_string())?; - // The created issue should appear in the filtered list. - if arr.iter().any(|i| { - i.get("url") - .and_then(|u| u.as_str()) - .is_some_and(|u| u.contains(&issue_id) || u.len() > 0) - }) || !arr.is_empty() - { + if !arr.is_empty() { Ok(()) } else { Err("filtered list is empty".to_string()) } }) .expect("issues list --project should return results (after retries)"); - - // Clean up. - delete_issue(&issue_id); - rt.block_on(async { - client.project_delete::(project_id).await.unwrap(); - }); } } From d2d9a9849a9a045e1aabe5a230c8549562d18504 Mon Sep 17 00:00:00 2001 From: Geoffrey Sechter Date: Sat, 28 Feb 2026 14:57:16 -0700 Subject: [PATCH 5/9] fix: use existing project in --project filter online test Newly created projects may not be immediately available for issue creation in Linear's API. Use an existing workspace project instead to avoid flakiness from API propagation delays. --- crates/lineark/tests/online.rs | 108 ++++++++++----------------------- 1 file changed, 33 insertions(+), 75 deletions(-) diff --git a/crates/lineark/tests/online.rs b/crates/lineark/tests/online.rs index a21542f..8f54116 100644 --- a/crates/lineark/tests/online.rs +++ b/crates/lineark/tests/online.rs @@ -3405,15 +3405,7 @@ mod online { fn issues_list_with_project_filter() { let token = api_token(); - // Get a team key. - let output = lineark() - .args(["--api-token", &token, "--format", "json", "teams", "list"]) - .output() - .unwrap(); - let teams: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); - let team_key = teams[0]["key"].as_str().unwrap().to_string(); - - // Create a project via CLI. + // Find an existing project that has at least one issue. let output = lineark() .args([ "--api-token", @@ -3421,67 +3413,24 @@ mod online { "--format", "json", "projects", - "create", - "[test] project filter test", - "--team", - &team_key, + "list", ]) .output() .unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); + let projects: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + let projects_arr = projects.as_array().expect("projects list should be array"); assert!( - output.status.success(), - "project creation should succeed.\nstdout: {stdout}\nstderr: {stderr}" + !projects_arr.is_empty(), + "workspace must have at least one project" ); - let project: serde_json::Value = serde_json::from_str(&stdout).unwrap(); - let project_id = project["id"].as_str().unwrap().to_string(); - let _project_guard = ProjectGuard { - token: token.clone(), - id: project_id.clone(), - }; - // Create an issue in that project. - // Linear may need a moment before the project is available for issue creation. - let issue_id = retry_with_backoff(8, || { - let output = lineark() - .args([ - "--api-token", - &token, - "--format", - "json", - "issues", - "create", - "[test] in project", - "--team", - &team_key, - "--project", - &project_id, - "--priority", - "4", - ]) - .output() - .unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - return Err(format!( - "issue creation failed.\nstdout: {stdout}\nstderr: {stderr}" - )); + // Try each project until we find one that returns issues with --project filter. + let mut found = false; + for project in projects_arr { + let project_name = project["name"].as_str().unwrap_or("").to_string(); + if project_name.is_empty() { + continue; } - let created: serde_json::Value = serde_json::from_str(&stdout).unwrap(); - Ok(created["id"].as_str().unwrap().to_string()) - }) - .expect("issue creation in project should succeed (after retries)"); - - let _issue_guard = IssueGuard { - token: token.clone(), - id: issue_id, - }; - - // List issues filtered by project name. - // Use retry since project association may need time to propagate. - retry_with_backoff(8, || { let output = lineark() .args([ "--api-token", @@ -3491,25 +3440,34 @@ mod online { "issues", "list", "--project", - "[test] project filter test", + &project_name, "--limit", - "50", + "5", ]) .output() .unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - return Err(format!("list failed.\nstdout: {stdout}\nstderr: {stderr}")); + continue; } + let stdout = String::from_utf8_lossy(&output.stdout); let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); - let arr = json.as_array().ok_or("not an array".to_string())?; - if !arr.is_empty() { - Ok(()) - } else { - Err("filtered list is empty".to_string()) + if let Some(arr) = json.as_array() { + if !arr.is_empty() { + // Verify all returned issues are in the expected JSON format. + for issue in arr { + assert!( + issue.get("identifier").is_some(), + "each issue should have an identifier" + ); + } + found = true; + break; + } } - }) - .expect("issues list --project should return results (after retries)"); + } + assert!( + found, + "at least one project should have issues in the workspace" + ); } } From b23515c1fc63889be9b9fb93cb19b11313a93fa1 Mon Sep 17 00:00:00 2001 From: Cadu Date: Fri, 6 Mar 2026 18:15:58 +0100 Subject: [PATCH 6/9] fix: make issues_list_with_project_filter test self-contained Instead of assuming the workspace already has projects (which fails in CI), the test now creates its own project and issue with RAII cleanup guards. Also adds proper status checks with readable error messages before parsing JSON output. --- crates/lineark/tests/online.rs | 125 ++++++++++++++++++++++++--------- 1 file changed, 92 insertions(+), 33 deletions(-) diff --git a/crates/lineark/tests/online.rs b/crates/lineark/tests/online.rs index 8f54116..7fb4c7d 100644 --- a/crates/lineark/tests/online.rs +++ b/crates/lineark/tests/online.rs @@ -3405,7 +3405,21 @@ mod online { fn issues_list_with_project_filter() { let token = api_token(); - // Find an existing project that has at least one issue. + // Get a team key to create resources against. + let output = lineark() + .args(["--api-token", &token, "--format", "json", "teams", "list"]) + .output() + .unwrap(); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "teams list should succeed.\nstdout: {stdout}\nstderr: {stderr}" + ); + let teams: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + let team_key = teams[0]["key"].as_str().unwrap().to_string(); + + // Create a project for the test. let output = lineark() .args([ "--api-token", @@ -3413,24 +3427,68 @@ mod online { "--format", "json", "projects", - "list", + "create", + "[test] CLI project filter", + "--team", + &team_key, ]) .output() - .unwrap(); - let projects: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); - let projects_arr = projects.as_array().expect("projects list should be array"); + .expect("failed to execute lineark"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); assert!( - !projects_arr.is_empty(), - "workspace must have at least one project" + output.status.success(), + "projects create should succeed.\nstdout: {stdout}\nstderr: {stderr}" ); + let created_project: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + let project_id = created_project["id"] + .as_str() + .expect("created project should have id") + .to_string(); + let project_name = created_project["name"] + .as_str() + .expect("created project should have name") + .to_string(); + let _project_guard = ProjectGuard { + token: token.clone(), + id: project_id, + }; - // Try each project until we find one that returns issues with --project filter. - let mut found = false; - for project in projects_arr { - let project_name = project["name"].as_str().unwrap_or("").to_string(); - if project_name.is_empty() { - continue; - } + // Create an issue inside that project. + let output = lineark() + .args([ + "--api-token", + &token, + "--format", + "json", + "issues", + "create", + "[test] project filter issue", + "--team", + &team_key, + "--project", + &project_name, + ]) + .output() + .expect("failed to execute lineark"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "issues create should succeed.\nstdout: {stdout}\nstderr: {stderr}" + ); + let created_issue: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + let issue_id = created_issue["id"] + .as_str() + .expect("created issue should have id") + .to_string(); + let _issue_guard = IssueGuard { + token: token.clone(), + id: issue_id, + }; + + // List issues with --project filter (retry for eventual consistency). + retry_with_backoff(5, || { let output = lineark() .args([ "--api-token", @@ -3447,27 +3505,28 @@ mod online { .output() .unwrap(); if !output.status.success() { - continue; + return Err(format!( + "issues list --project failed: {}", + String::from_utf8_lossy(&output.stderr) + )); } let stdout = String::from_utf8_lossy(&output.stdout); - let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); - if let Some(arr) = json.as_array() { - if !arr.is_empty() { - // Verify all returned issues are in the expected JSON format. - for issue in arr { - assert!( - issue.get("identifier").is_some(), - "each issue should have an identifier" - ); - } - found = true; - break; - } + let json: serde_json::Value = + serde_json::from_str(&stdout).map_err(|e| format!("invalid JSON: {e}"))?; + let arr = json + .as_array() + .ok_or_else(|| "expected array".to_string())?; + if arr.is_empty() { + return Err("no issues returned yet".to_string()); } - } - assert!( - found, - "at least one project should have issues in the workspace" - ); + for issue in arr { + assert!( + issue.get("identifier").is_some(), + "each issue should have an identifier" + ); + } + Ok(()) + }) + .expect("issues list --project should return at least one issue"); } } From 972753f5db11096626895b92f7407576731b4aaa Mon Sep 17 00:00:00 2001 From: Cadu Date: Fri, 6 Mar 2026 18:21:20 +0100 Subject: [PATCH 7/9] docs: add online test conventions to CLAUDE.md Document that online tests must be self-contained: create their own resources with RAII guards, use retry_with_backoff for eventual consistency, and check command status before parsing JSON output. --- CLAUDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 45d8156..d775a3d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,6 +47,8 @@ cargo test -p lineark-sdk --test online -- --test-threads=1 cargo test -p lineark --test online -- --test-threads=1 ``` +**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 `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 `schema/schema.graphql` is a vendored copy of Linear's public GraphQL schema (SDL). It's checked in for reproducible builds and reviewable diffs. To update it: From 3dd2de4aa82877d3612b24605487750cb6ed2db2 Mon Sep 17 00:00:00 2001 From: Cadu Date: Fri, 6 Mar 2026 18:33:51 +0100 Subject: [PATCH 8/9] fix: use unique project name in project filter test Avoid name collisions with zombie resources from previously-failed test runs by appending a UUID suffix. Also document this pattern in CLAUDE.md. --- CLAUDE.md | 2 +- crates/lineark/tests/online.rs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d775a3d..f530e0d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,7 +47,7 @@ cargo test -p lineark-sdk --test online -- --test-threads=1 cargo test -p lineark --test online -- --test-threads=1 ``` -**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 `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. 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). ## Updating the schema diff --git a/crates/lineark/tests/online.rs b/crates/lineark/tests/online.rs index 7fb4c7d..4a05d81 100644 --- a/crates/lineark/tests/online.rs +++ b/crates/lineark/tests/online.rs @@ -3419,7 +3419,11 @@ mod online { let teams: serde_json::Value = serde_json::from_str(&stdout).unwrap(); let team_key = teams[0]["key"].as_str().unwrap().to_string(); - // Create a project for the test. + // Create a project for the test (unique name to avoid conflicts). + let project_label = format!( + "[test] CLI project filter {}", + &uuid::Uuid::new_v4().to_string()[..8] + ); let output = lineark() .args([ "--api-token", @@ -3428,7 +3432,7 @@ mod online { "json", "projects", "create", - "[test] CLI project filter", + &project_label, "--team", &team_key, ]) From c1a3dcbf72d79d377eba204433030ea3b16fddb9 Mon Sep 17 00:00:00 2001 From: Cadu Date: Fri, 6 Mar 2026 18:46:21 +0100 Subject: [PATCH 9/9] fix: use unique names for all test resources in online tests Append UUID suffixes to every hardcoded test resource name (issues, projects, milestones, documents) to prevent name collisions from zombie resources left by previously-failed test runs. --- crates/lineark/tests/online.rs | 131 ++++++++++++++++++++++++--------- 1 file changed, 95 insertions(+), 36 deletions(-) diff --git a/crates/lineark/tests/online.rs b/crates/lineark/tests/online.rs index 4a05d81..f06f897 100644 --- a/crates/lineark/tests/online.rs +++ b/crates/lineark/tests/online.rs @@ -424,6 +424,10 @@ mod online { #[test_with::runtime_ignore_if(no_online_test_token)] fn issues_create_update_and_archive() { let token = api_token(); + let unique_name = format!( + "[test] CLI create+update {}", + &uuid::Uuid::new_v4().to_string()[..8] + ); // Get the first team key. let output = lineark() @@ -442,7 +446,7 @@ mod online { "json", "issues", "create", - "[test] CLI create+update", + &unique_name, "--team", &team_key, "--priority", @@ -480,7 +484,7 @@ mod online { "--priority", "2", "--title", - "[test] CLI create+update — updated", + &format!("{unique_name} — updated"), ]) .output() .expect("failed to execute lineark"); @@ -508,6 +512,10 @@ mod online { #[test_with::runtime_ignore_if(no_online_test_token)] fn issues_archive_and_unarchive_cycle() { let token = api_token(); + let unique_name = format!( + "[test] CLI archive/unarchive {}", + &uuid::Uuid::new_v4().to_string()[..8] + ); // Get a team key. let output = lineark() @@ -526,7 +534,7 @@ mod online { "json", "issues", "create", - "[test] CLI archive/unarchive", + &unique_name, "--team", &team_key, "--priority", @@ -660,6 +668,10 @@ mod online { #[test_with::runtime_ignore_if(no_online_test_token)] fn issues_unarchive_by_human_identifier() { let token = api_token(); + let unique_name = format!( + "[test] unarchive by identifier {}", + &uuid::Uuid::new_v4().to_string()[..8] + ); // Get a team key. let output = lineark() @@ -678,7 +690,7 @@ mod online { "json", "issues", "create", - "[test] unarchive by identifier", + &unique_name, "--team", &team_key, "--priority", @@ -756,6 +768,10 @@ mod online { #[test_with::runtime_ignore_if(no_online_test_token)] fn issues_delete_permanently() { let token = api_token(); + let unique_name = format!( + "[test] CLI issues delete {}", + &uuid::Uuid::new_v4().to_string()[..8] + ); // Get a team key. let output = lineark() @@ -774,7 +790,7 @@ mod online { "json", "issues", "create", - "[test] CLI issues delete", + &unique_name, "--team", &team_key, "--priority", @@ -824,6 +840,10 @@ mod online { #[test_with::runtime_ignore_if(no_online_test_token)] fn issues_delete_trash_and_verify() { let token = api_token(); + let unique_name = format!( + "[test] CLI issues trash {}", + &uuid::Uuid::new_v4().to_string()[..8] + ); // Get a team key. let output = lineark() @@ -842,7 +862,7 @@ mod online { "json", "issues", "create", - "[test] CLI issues trash", + &unique_name, "--team", &team_key, "--priority", @@ -915,6 +935,9 @@ mod online { #[test_with::runtime_ignore_if(no_online_test_token)] fn documents_create_read_update_and_delete() { let token = api_token(); + let suffix = &uuid::Uuid::new_v4().to_string()[..8]; + let issue_name = format!("[test] doc parent issue {suffix}"); + let doc_name = format!("[test] CLI documents CRUD {suffix}"); // Get a team key first (documents require a parent like project/issue/team). // Create an issue to associate the document with. @@ -933,7 +956,7 @@ mod online { "json", "issues", "create", - "[test] doc parent issue", + &issue_name, "--team", &team_key, "--priority", @@ -959,7 +982,7 @@ mod online { "documents", "create", "--title", - "[test] CLI documents CRUD", + &doc_name, "--content", "Automated CLI test document.", "--issue", @@ -998,10 +1021,7 @@ mod online { "documents read should succeed.\nstdout: {stdout}\nstderr: {stderr}" ); let read_doc: serde_json::Value = serde_json::from_str(&stdout).unwrap(); - assert_eq!( - read_doc["title"].as_str(), - Some("[test] CLI documents CRUD") - ); + assert_eq!(read_doc["title"].as_str(), Some(doc_name.as_str())); // Update the document. let output = lineark() @@ -1014,7 +1034,7 @@ mod online { "update", doc_id, "--title", - "[test] CLI documents CRUD — updated", + &format!("{doc_name} — updated"), ]) .output() .expect("failed to execute lineark"); @@ -1391,6 +1411,7 @@ mod online { #[test_with::runtime_ignore_if(no_online_test_token)] fn issues_read_shows_relations() { let token = api_token(); + let suffix = &uuid::Uuid::new_v4().to_string()[..8]; // Get a team key. let output = lineark() @@ -1409,7 +1430,7 @@ mod online { "json", "issues", "create", - "[test] relation parent", + &format!("[test] relation parent {suffix}"), "--team", &team_key, "--priority", @@ -1433,7 +1454,7 @@ mod online { "json", "issues", "create", - "[test] relation child", + &format!("[test] relation child {suffix}"), "--team", &team_key, "--priority", @@ -1522,6 +1543,7 @@ mod online { #[test_with::runtime_ignore_if(no_online_test_token)] fn issues_read_shows_children_and_comments() { let token = api_token(); + let suffix = &uuid::Uuid::new_v4().to_string()[..8]; // Get a team key. let output = lineark() @@ -1540,7 +1562,7 @@ mod online { "json", "issues", "create", - "[test] parent with children", + &format!("[test] parent with children {suffix}"), "--team", &team_key, "-p", @@ -1568,7 +1590,7 @@ mod online { "json", "issues", "create", - "[test] child issue", + &format!("[test] child issue {suffix}"), "--team", &team_key, "-p", @@ -1751,6 +1773,7 @@ mod online { #[test_with::runtime_ignore_if(no_online_test_token)] fn issues_create_with_parent_and_clear_parent() { let token = api_token(); + let suffix = &uuid::Uuid::new_v4().to_string()[..8]; // Get a team key. let output = lineark() @@ -1769,7 +1792,7 @@ mod online { "json", "issues", "create", - "[test] clear-parent parent", + &format!("[test] clear-parent parent {suffix}"), "--team", &team_key, "-p", @@ -1794,7 +1817,7 @@ mod online { "json", "issues", "create", - "[test] clear-parent child", + &format!("[test] clear-parent child {suffix}"), "--team", &team_key, "-p", @@ -1864,6 +1887,10 @@ mod online { #[test_with::runtime_ignore_if(no_online_test_token)] fn project_milestones_full_crud() { let token = api_token(); + let suffix = &uuid::Uuid::new_v4().to_string()[..8]; + let project_label = format!("[test] milestones CRUD project {suffix}"); + let milestone_name = format!("[test] Beta Release {suffix}"); + let milestone_updated = format!("[test] GA Release {suffix}"); // Get a team ID (projectCreate requires teamIds). let output = lineark() @@ -1878,7 +1905,7 @@ mod online { let rt = tokio::runtime::Runtime::new().unwrap(); let project: Project = rt.block_on(async { let input = ProjectCreateInput { - name: Some("[test] milestones CRUD project".to_string()), + name: Some(project_label.clone()), team_ids: Some(vec![team_id]), ..Default::default() }; @@ -1899,7 +1926,7 @@ mod online { "json", "project-milestones", "create", - "[test] Beta Release", + &milestone_name, "--project", &project_id, "--target-date", @@ -1966,7 +1993,7 @@ mod online { "json", "project-milestones", "read", - "[test] Beta Release", + &milestone_name, "--project", &project_id, ]) @@ -1996,7 +2023,7 @@ mod online { "update", &milestone_id, "--name", - "[test] GA Release", + &milestone_updated, "--target-date", "2027-03-15", ]) @@ -2011,7 +2038,7 @@ mod online { let updated: serde_json::Value = serde_json::from_str(&stdout).unwrap(); assert_eq!( updated["name"].as_str(), - Some("[test] GA Release"), + Some(milestone_updated.as_str()), "updated name should match" ); @@ -2046,6 +2073,10 @@ mod online { #[test_with::runtime_ignore_if(no_online_test_token)] fn projects_create_and_delete() { let token = api_token(); + let unique_name = format!( + "[test] CLI projects create {}", + &uuid::Uuid::new_v4().to_string()[..8] + ); // Get a team key. let output = lineark() @@ -2064,7 +2095,7 @@ mod online { "json", "projects", "create", - "[test] CLI projects create", + &unique_name, "--team", &team_key, "--description", @@ -2201,6 +2232,10 @@ mod online { #[test_with::runtime_ignore_if(no_online_test_token)] fn projects_read_by_id_and_name() { let token = api_token(); + let unique_name = format!( + "[test] CLI projects read {}", + &uuid::Uuid::new_v4().to_string()[..8] + ); // Get a team key. let output = lineark() @@ -2219,7 +2254,7 @@ mod online { "json", "projects", "create", - "[test] CLI projects read", + &unique_name, "--team", &team_key, "--lead", @@ -2263,7 +2298,7 @@ mod online { ); let detail: serde_json::Value = serde_json::from_str(&stdout).unwrap(); assert_eq!(detail["id"].as_str(), Some(project_id.as_str())); - assert_eq!(detail["name"].as_str(), Some("[test] CLI projects read")); + assert_eq!(detail["name"].as_str(), Some(unique_name.as_str())); assert!( detail.get("lead").is_some() && !detail["lead"].is_null(), "read output should have a lead (set to me)" @@ -2291,7 +2326,7 @@ mod online { "json", "projects", "read", - "[test] CLI projects read", + &unique_name, ]) .output() .expect("failed to execute lineark"); @@ -2323,6 +2358,10 @@ mod online { #[test_with::runtime_ignore_if(no_online_test_token)] fn projects_create_with_members_and_read_back() { let token = api_token(); + let unique_name = format!( + "[test] CLI members test {}", + &uuid::Uuid::new_v4().to_string()[..8] + ); // Get a team key and find two users. let output = lineark() @@ -2341,7 +2380,7 @@ mod online { "json", "projects", "create", - "[test] CLI members test", + &unique_name, "--team", &team_key, "--lead", @@ -2423,6 +2462,10 @@ mod online { #[test_with::runtime_ignore_if(no_online_test_token)] fn issues_create_with_assignee_me() { let token = api_token(); + let unique_name = format!( + "[test] CLI assignee me {}", + &uuid::Uuid::new_v4().to_string()[..8] + ); // Get a team key. let output = lineark() @@ -2449,7 +2492,7 @@ mod online { "json", "issues", "create", - "[test] CLI assignee me", + &unique_name, "--team", &team_key, "--assignee", @@ -2501,6 +2544,10 @@ mod online { #[test_with::runtime_ignore_if(no_online_test_token)] fn issues_update_with_assignee_me() { let token = api_token(); + let unique_name = format!( + "[test] CLI update assignee me {}", + &uuid::Uuid::new_v4().to_string()[..8] + ); // Get a team key. let output = lineark() @@ -2527,7 +2574,7 @@ mod online { "json", "issues", "create", - "[test] CLI update assignee me", + &unique_name, "--team", &team_key, "--priority", @@ -2604,6 +2651,10 @@ mod online { #[test_with::runtime_ignore_if(no_online_test_token)] fn comments_create_on_issue() { let token = api_token(); + let unique_name = format!( + "[test] CLI comments_create {}", + &uuid::Uuid::new_v4().to_string()[..8] + ); // Get a team key. let output = lineark() @@ -2622,7 +2673,7 @@ mod online { "json", "issues", "create", - "[test] CLI comments_create", + &unique_name, "--team", &team_key, "--priority", @@ -2674,6 +2725,10 @@ mod online { #[test_with::runtime_ignore_if(no_online_test_token)] fn comments_create_and_delete() { let token = api_token(); + let unique_name = format!( + "[test] CLI comments_delete {}", + &uuid::Uuid::new_v4().to_string()[..8] + ); // Get a team key. let output = lineark() @@ -2692,7 +2747,7 @@ mod online { "json", "issues", "create", - "[test] CLI comments_delete", + &unique_name, "--team", &team_key, "--priority", @@ -3126,6 +3181,7 @@ mod online { token: &str, team_key: &str, ) -> ((String, IssueGuard), (String, IssueGuard)) { + let suffix = &uuid::Uuid::new_v4().to_string()[..8]; let out1 = lineark() .args([ "--api-token", @@ -3134,7 +3190,7 @@ mod online { "json", "issues", "create", - "[test] relation issue A", + &format!("[test] relation issue A {suffix}"), "--team", team_key, "--priority", @@ -3158,7 +3214,7 @@ mod online { "json", "issues", "create", - "[test] relation issue B", + &format!("[test] relation issue B {suffix}"), "--team", team_key, "--priority", @@ -3467,7 +3523,10 @@ mod online { "json", "issues", "create", - "[test] project filter issue", + &format!( + "[test] project filter issue {}", + &uuid::Uuid::new_v4().to_string()[..8] + ), "--team", &team_key, "--project",