mod common;
use common::cli::{BrWorkspace, extract_json_payload, run_br};
use serde_json::Value;
fn parse_created_id(stdout: &str) -> String {
let line = stdout.lines().next().unwrap_or("");
let normalized = line.strip_prefix("✓ ").unwrap_or(line);
let id_part = normalized
.strip_prefix("Created ")
.and_then(|rest| rest.split(':').next())
.unwrap_or("");
id_part.trim().to_string()
}
#[test]
fn e2e_comments_add_single_and_list() {
let _log = common::test_log("e2e_comments_add_single_and_list");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let create = run_br(&workspace, ["create", "Test issue for comments"], "create");
assert!(create.status.success(), "create failed: {}", create.stderr);
let id = parse_created_id(&create.stdout);
assert!(!id.is_empty(), "missing created id");
let add = run_br(
&workspace,
["comments", "add", &id, "This is my first comment"],
"add_comment",
);
assert!(add.status.success(), "add comment failed: {}", add.stderr);
let list = run_br(&workspace, ["comments", "list", &id], "list_comments");
assert!(
list.status.success(),
"list comments failed: {}",
list.stderr
);
assert!(
list.stdout.contains("This is my first comment"),
"comment not found in list output"
);
}
#[test]
fn e2e_comments_add_multiple_verify_order() {
let _log = common::test_log("e2e_comments_add_multiple_verify_order");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let create = run_br(&workspace, ["create", "Multiple comments test"], "create");
assert!(create.status.success(), "create failed: {}", create.stderr);
let id = parse_created_id(&create.stdout);
let add1 = run_br(
&workspace,
["comments", "add", &id, "First comment"],
"add_comment1",
);
assert!(
add1.status.success(),
"add comment 1 failed: {}",
add1.stderr
);
let add2 = run_br(
&workspace,
["comments", "add", &id, "Second comment"],
"add_comment2",
);
assert!(
add2.status.success(),
"add comment 2 failed: {}",
add2.stderr
);
let add3 = run_br(
&workspace,
["comments", "add", &id, "Third comment"],
"add_comment3",
);
assert!(
add3.status.success(),
"add comment 3 failed: {}",
add3.stderr
);
let list = run_br(&workspace, ["comments", "list", &id, "--json"], "list_json");
assert!(list.status.success(), "list json failed: {}", list.stderr);
let payload = extract_json_payload(&list.stdout);
let comments: Vec<Value> = serde_json::from_str(&payload).expect("parse comments json");
assert_eq!(comments.len(), 3, "should have 3 comments");
let texts: Vec<&str> = comments.iter().filter_map(|c| c["text"].as_str()).collect();
assert_eq!(texts[0], "First comment");
assert_eq!(texts[1], "Second comment");
assert_eq!(texts[2], "Third comment");
}
#[test]
fn e2e_comments_list_json_structure() {
let _log = common::test_log("e2e_comments_list_json_structure");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let create = run_br(&workspace, ["create", "JSON structure test"], "create");
assert!(create.status.success(), "create failed: {}", create.stderr);
let id = parse_created_id(&create.stdout);
let add = run_br(
&workspace,
[
"comments",
"add",
&id,
"--author",
"test-user",
"JSON structure comment",
],
"add_comment",
);
assert!(add.status.success(), "add comment failed: {}", add.stderr);
let list = run_br(&workspace, ["comments", "list", &id, "--json"], "list_json");
assert!(list.status.success(), "list json failed: {}", list.stderr);
let payload = extract_json_payload(&list.stdout);
let comments: Vec<Value> = serde_json::from_str(&payload).expect("parse comments json");
assert_eq!(comments.len(), 1, "should have 1 comment");
let comment = &comments[0];
assert!(
comment["id"].is_number() || comment["id"].is_string(),
"comment should have id"
);
assert_eq!(comment["text"], "JSON structure comment");
assert_eq!(comment["author"], "test-user"); assert!(
comment["created_at"].is_string(),
"comment should have created_at"
);
}
#[test]
fn e2e_comments_add_to_existing() {
let _log = common::test_log("e2e_comments_add_to_existing");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let create = run_br(&workspace, ["create", "Existing comments test"], "create");
assert!(create.status.success(), "create failed: {}", create.stderr);
let id = parse_created_id(&create.stdout);
let add1 = run_br(
&workspace,
["comments", "add", &id, "Existing comment"],
"add_comment1",
);
assert!(
add1.status.success(),
"add comment 1 failed: {}",
add1.stderr
);
let list1 = run_br(&workspace, ["comments", "list", &id, "--json"], "list1");
assert!(list1.status.success(), "list1 failed: {}", list1.stderr);
let payload1 = extract_json_payload(&list1.stdout);
let comments1: Vec<Value> = serde_json::from_str(&payload1).expect("parse json");
assert_eq!(comments1.len(), 1, "should have 1 comment");
let add2 = run_br(
&workspace,
["comments", "add", &id, "New comment added"],
"add_comment2",
);
assert!(
add2.status.success(),
"add comment 2 failed: {}",
add2.stderr
);
let list2 = run_br(&workspace, ["comments", "list", &id, "--json"], "list2");
assert!(list2.status.success(), "list2 failed: {}", list2.stderr);
let payload2 = extract_json_payload(&list2.stdout);
let comments2: Vec<Value> = serde_json::from_str(&payload2).expect("parse json");
assert_eq!(comments2.len(), 2, "should have 2 comments");
}
#[test]
fn e2e_comments_add_nonexistent_issue() {
let _log = common::test_log("e2e_comments_add_nonexistent_issue");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let add = run_br(
&workspace,
["comments", "add", "bd-nonexistent", "This should fail"],
"add_nonexistent",
);
assert!(
!add.status.success(),
"add comment to non-existent issue should fail"
);
assert!(
add.stderr.contains("not found")
|| add.stderr.contains("Issue")
|| add.stderr.contains("error"),
"error message should indicate issue not found: {}",
add.stderr
);
}
#[test]
fn e2e_comments_add_empty() {
let _log = common::test_log("e2e_comments_add_empty");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let create = run_br(&workspace, ["create", "Empty comment test"], "create");
assert!(create.status.success(), "create failed: {}", create.stderr);
let id = parse_created_id(&create.stdout);
let add = run_br(&workspace, ["comments", "add", &id], "add_empty");
if add.status.success() {
let list = run_br(
&workspace,
["comments", "list", &id, "--json"],
"list_empty",
);
let payload = extract_json_payload(&list.stdout);
let comments: Vec<Value> = serde_json::from_str(&payload).unwrap_or_default();
assert!(
comments.is_empty()
|| comments
.iter()
.all(|c| c["text"].as_str().is_none_or(str::is_empty)),
"empty comment handling"
);
} else {
assert!(
add.stderr.contains("empty")
|| add.stderr.contains("required")
|| add.stderr.contains("text"),
"error message should indicate empty comment rejected: {}",
add.stderr
);
}
}
#[test]
fn e2e_comments_list_empty() {
let _log = common::test_log("e2e_comments_list_empty");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let create = run_br(&workspace, ["create", "No comments issue"], "create");
assert!(create.status.success(), "create failed: {}", create.stderr);
let id = parse_created_id(&create.stdout);
let list = run_br(
&workspace,
["comments", "list", &id, "--json"],
"list_empty",
);
assert!(
list.status.success(),
"list empty comments failed: {}",
list.stderr
);
let payload = extract_json_payload(&list.stdout);
let comments: Vec<Value> = serde_json::from_str(&payload).expect("parse json");
assert!(comments.is_empty(), "should have 0 comments");
}
#[test]
fn e2e_comments_special_characters() {
let _log = common::test_log("e2e_comments_special_characters");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let create = run_br(&workspace, ["create", "Special chars test"], "create");
assert!(create.status.success(), "create failed: {}", create.stderr);
let id = parse_created_id(&create.stdout);
let special_text = "Quote: \"hello\" and apostrophe's and emoji: 🚀";
let add = run_br(
&workspace,
["comments", "add", &id, "--message", special_text],
"add_special",
);
assert!(
add.status.success(),
"add special comment failed: {}",
add.stderr
);
let list = run_br(
&workspace,
["comments", "list", &id, "--json"],
"list_special",
);
assert!(list.status.success(), "list failed: {}", list.stderr);
let payload = extract_json_payload(&list.stdout);
let comments: Vec<Value> = serde_json::from_str(&payload).expect("parse json");
assert_eq!(comments.len(), 1, "should have 1 comment");
let text = comments[0]["text"].as_str().expect("text field");
assert!(text.contains("Quote:"), "should contain quote");
assert!(text.contains("hello"), "should contain quoted text");
assert!(
text.contains("apostrophe") || text.contains('\''),
"should contain apostrophe"
);
}
#[test]
fn e2e_comments_long_text() {
let _log = common::test_log("e2e_comments_long_text");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let create = run_br(&workspace, ["create", "Long comment test"], "create");
assert!(create.status.success(), "create failed: {}", create.stderr);
let id = parse_created_id(&create.stdout);
let long_text = "x".repeat(10_000);
let add = run_br(
&workspace,
["comments", "add", &id, "--message", &long_text],
"add_long",
);
assert!(
add.status.success(),
"add long comment failed: {}",
add.stderr
);
let list = run_br(&workspace, ["comments", "list", &id, "--json"], "list_long");
assert!(list.status.success(), "list failed: {}", list.stderr);
let payload = extract_json_payload(&list.stdout);
let comments: Vec<Value> = serde_json::from_str(&payload).expect("parse json");
assert_eq!(comments.len(), 1, "should have 1 comment");
let text = comments[0]["text"].as_str().expect("text field");
assert_eq!(text.len(), 10_000, "comment should be 10KB");
}
#[test]
fn e2e_comments_on_closed_issue() {
let _log = common::test_log("e2e_comments_on_closed_issue");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let create = run_br(&workspace, ["create", "Closed issue test"], "create");
assert!(create.status.success(), "create failed: {}", create.stderr);
let id = parse_created_id(&create.stdout);
let close = run_br(
&workspace,
["close", &id, "--reason", "Testing closed comments"],
"close_issue",
);
assert!(close.status.success(), "close failed: {}", close.stderr);
let add = run_br(
&workspace,
["comments", "add", &id, "Comment on closed issue"],
"add_closed",
);
assert!(
add.status.success(),
"add comment to closed issue failed: {}",
add.stderr
);
let list = run_br(
&workspace,
["comments", "list", &id, "--json"],
"list_closed",
);
assert!(list.status.success(), "list failed: {}", list.stderr);
let payload = extract_json_payload(&list.stdout);
let comments: Vec<Value> = serde_json::from_str(&payload).expect("parse json");
assert_eq!(comments.len(), 1, "should have 1 comment");
assert_eq!(comments[0]["text"], "Comment on closed issue");
}
#[test]
fn e2e_comments_add_json_output() {
let _log = common::test_log("e2e_comments_add_json_output");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let create = run_br(&workspace, ["create", "JSON add test"], "create");
assert!(create.status.success(), "create failed: {}", create.stderr);
let id = parse_created_id(&create.stdout);
let add = run_br(
&workspace,
["comments", "add", &id, "--json", "JSON output comment"],
"add_json",
);
assert!(add.status.success(), "add json failed: {}", add.stderr);
let payload = extract_json_payload(&add.stdout);
let result: Value = serde_json::from_str(&payload).expect("parse add json");
assert!(
result.is_object() || result.is_array(),
"add --json should return structured output"
);
}
#[test]
fn e2e_comments_shorthand() {
let _log = common::test_log("e2e_comments_shorthand");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let create = run_br(&workspace, ["create", "Shorthand test"], "create");
assert!(create.status.success(), "create failed: {}", create.stderr);
let id = parse_created_id(&create.stdout);
let add = run_br(
&workspace,
["comments", "add", &id, "Shorthand comment"],
"add_comment",
);
assert!(add.status.success(), "add comment failed: {}", add.stderr);
let list = run_br(&workspace, ["comments", &id], "list_shorthand");
assert!(
list.status.success(),
"list shorthand failed: {}",
list.stderr
);
assert!(
list.stdout.contains("Shorthand comment"),
"shorthand should list comments"
);
}
#[test]
fn e2e_comments_sync_roundtrip() {
let _log = common::test_log("e2e_comments_sync_roundtrip");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let create = run_br(&workspace, ["create", "Sync roundtrip test"], "create");
assert!(create.status.success(), "create failed: {}", create.stderr);
let id = parse_created_id(&create.stdout);
let add1 = run_br(
&workspace,
["comments", "add", &id, "First sync comment"],
"add_comment1",
);
assert!(
add1.status.success(),
"add comment 1 failed: {}",
add1.stderr
);
let add2 = run_br(
&workspace,
["comments", "add", &id, "Second sync comment"],
"add_comment2",
);
assert!(
add2.status.success(),
"add comment 2 failed: {}",
add2.stderr
);
let flush = run_br(&workspace, ["sync", "--flush-only"], "sync_flush");
assert!(
flush.status.success(),
"sync flush failed: {}",
flush.stderr
);
let workspace2 = BrWorkspace::new();
let init2 = run_br(&workspace2, ["init"], "init2");
assert!(init2.status.success(), "init2 failed: {}", init2.stderr);
let jsonl_src = workspace.root.join(".beads").join("issues.jsonl");
let jsonl_dst = workspace2.root.join(".beads").join("issues.jsonl");
std::fs::copy(&jsonl_src, &jsonl_dst).expect("copy jsonl");
let import = run_br(
&workspace2,
["sync", "--import-only", "--force"],
"sync_import",
);
assert!(import.status.success(), "import failed: {}", import.stderr);
let list = run_br(
&workspace2,
["comments", "list", &id, "--json"],
"list_after_import",
);
assert!(
list.status.success(),
"list after import failed: {}",
list.stderr
);
let payload = extract_json_payload(&list.stdout);
let comments: Vec<Value> = serde_json::from_str(&payload).expect("parse json");
assert_eq!(comments.len(), 2, "should have 2 comments after import");
let texts: Vec<&str> = comments.iter().filter_map(|c| c["text"].as_str()).collect();
assert!(texts.contains(&"First sync comment"));
assert!(texts.contains(&"Second sync comment"));
}