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
4 changes: 4 additions & 0 deletions crates/lineark-sdk/src/generated/client_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ impl Client {
) -> Result<T, LinearError> {
crate::generated::mutations::comment_create::<T>(self, input).await
}
/// Deletes a comment.
pub async fn comment_delete(&self, id: String) -> Result<serde_json::Value, LinearError> {
crate::generated::mutations::comment_delete(self, id).await
}
/// Creates a new project.
///
/// Full type: [`Project`](super::types::Project)
Expand Down
11 changes: 11 additions & 0 deletions crates/lineark-sdk/src/generated/mutations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ pub async fn comment_create<
.execute_mutation::<T>(&query, variables, "commentCreate", "comment")
.await
}
/// Deletes a comment.
pub async fn comment_delete(client: &Client, id: String) -> Result<serde_json::Value, LinearError> {
let variables = serde_json::json!({ "id" : id });
let response_parts: Vec<String> = vec!["success".to_string(), "entityId".to_string()];
let query = String::from("mutation CommentDelete($id: String!) { commentDelete(id: $id) { ")
+ &response_parts.join(" ")
+ " } }";
client
.execute::<serde_json::Value>(&query, variables, "commentDelete")
.await
}
/// Creates a new project.
///
/// Full type: [`Project`](super::types::Project)
Expand Down
16 changes: 16 additions & 0 deletions crates/lineark/src/commands/comments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ pub enum CommentsAction {
#[arg(long)]
body: String,
},
/// Delete a comment.
///
/// Examples:
/// lineark comments delete <COMMENT-UUID>
Delete {
/// Comment UUID.
id: String,
},
}

/// Lean result type for comment mutations.
Expand Down Expand Up @@ -58,6 +66,14 @@ pub async fn run(cmd: CommentsCmd, client: &Client, format: Format) -> anyhow::R

output::print_one(&comment, format);
}
CommentsAction::Delete { id } => {
client
.comment_delete(id)
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;

output::print_one(&serde_json::json!({ "success": true }), format);
}
}
Ok(())
}
1 change: 1 addition & 0 deletions crates/lineark/src/commands/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ COMMANDS:
lineark issues delete <IDENTIFIER> Delete (trash) an issue
[--permanently] Permanently delete instead of trashing
lineark comments create <ISSUE-ID> --body TEXT Comment on an issue
lineark comments delete <COMMENT-UUID> Delete a comment
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)
Expand Down
29 changes: 29 additions & 0 deletions crates/lineark/tests/offline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -743,3 +743,32 @@ fn usage_includes_self_update_commands() {
.stdout(predicate::str::contains("self update"))
.stdout(predicate::str::contains("--check"));
}

// ── Comments delete ─────────────────────────────────────────────────────────

#[test]
fn comments_help_shows_delete_subcommand() {
lineark()
.args(["comments", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("delete"));
}

#[test]
fn comments_delete_help_shows_id_arg() {
lineark()
.args(["comments", "delete", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("<ID>"));
}

#[test]
fn usage_includes_comments_delete() {
lineark()
.arg("usage")
.assert()
.success()
.stdout(predicate::str::contains("comments delete"));
}
133 changes: 133 additions & 0 deletions crates/lineark/tests/online.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2508,4 +2508,137 @@ mod online {
// Clean up: permanently delete the issue.
delete_issue(issue_id);
}

// ── Comments create and delete ──────────────────────────────────────────

#[test_with::runtime_ignore_if(no_online_test_token)]
fn comments_create_and_delete() {
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 an issue to comment on.
let output = lineark()
.args([
"--api-token",
&token,
"--format",
"json",
"issues",
"create",
"[test] CLI comments_delete",
"--team",
&team_key,
"--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()
.expect("created issue should have id (UUID)")
.to_string();

// Create a comment.
let output = lineark()
.args([
"--api-token",
&token,
"--format",
"json",
"comments",
"create",
&issue_id,
"--body",
"Comment that will be deleted.",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"comment create should succeed.\nstdout: {stdout}\nstderr: {stderr}"
);
let comment: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let comment_id = comment["id"]
.as_str()
.expect("comment should have an id")
.to_string();

// Delete the comment.
let output = lineark()
.args([
"--api-token",
&token,
"--format",
"json",
"comments",
"delete",
&comment_id,
])
.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(),
"comments delete should succeed.\nstdout: {stdout}\nstderr: {stderr}"
);
// Verify the delete response indicates success.
let delete_result: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(
delete_result["success"].as_bool(),
Some(true),
"delete response should have success: true"
);

// Verify the comment is gone from the issue.
retry_with_backoff(8, || {
let output = lineark()
.args([
"--api-token",
&token,
"--format",
"json",
"issues",
"read",
&issue_id,
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
if !output.status.success() {
return Err(format!("issues read failed: {stdout}"));
}
let detail: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let comments = detail
.get("comments")
.and_then(|c| c.get("nodes"))
.and_then(|n| n.as_array());
let Some(comments) = comments else {
return Err("comments field missing".to_string());
};
let has_deleted = comments
.iter()
.any(|c| c["id"].as_str() == Some(&comment_id));
if has_deleted {
Err("deleted comment still present".to_string())
} else {
Ok(())
}
})
.expect("comment should be gone after deletion (after retries)");

// Clean up: permanently delete the issue.
delete_issue(&issue_id);
}
}
1 change: 1 addition & 0 deletions schema/operations.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ projectMilestone = true
issueCreate = true
issueUpdate = true
commentCreate = true
commentDelete = true

# Phase 3 — Issue lifecycle
issueArchive = true
Expand Down
Loading