#![allow(clippy::similar_names)]
mod common;
use common::cli::{BrWorkspace, extract_issues_array, extract_json_payload, run_br};
use serde_json::Value;
use std::fs;
use tracing::info;
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_dep_cycles_default_hides_closed_archive_and_include_closed_exposes_it() {
common::init_test_logging();
info!("e2e_dep_cycles_default_hides_closed_archive_and_include_closed_exposes_it: starting");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init_closed_archive_cycles");
assert!(init.status.success(), "init failed: {}", init.stderr);
let issue_a = run_br(&workspace, ["create", "Archived cycle A"], "create_a");
assert!(
issue_a.status.success(),
"create A failed: {}",
issue_a.stderr
);
let issue_a_id = parse_created_id(&issue_a.stdout);
let issue_b = run_br(&workspace, ["create", "Archived cycle B"], "create_b");
assert!(
issue_b.status.success(),
"create B failed: {}",
issue_b.stderr
);
let issue_b_id = parse_created_id(&issue_b.stdout);
let add_a_b = run_br(
&workspace,
["dep", "add", &issue_a_id, &issue_b_id, "-t", "related"],
"add_a_b_related",
);
assert!(
add_a_b.status.success(),
"add A->B failed: {}",
add_a_b.stderr
);
let add_b_a = run_br(
&workspace,
["dep", "add", &issue_b_id, &issue_a_id, "-t", "related"],
"add_b_a_related",
);
assert!(
add_b_a.status.success(),
"add B->A failed: {}",
add_b_a.stderr
);
let close_a = run_br(&workspace, ["close", &issue_a_id], "close_a");
assert!(
close_a.status.success(),
"close A failed: {}",
close_a.stderr
);
let close_b = run_br(&workspace, ["close", &issue_b_id], "close_b");
assert!(
close_b.status.success(),
"close B failed: {}",
close_b.stderr
);
let active_only = run_br(
&workspace,
["dep", "cycles", "--json"],
"cycles_active_only",
);
assert!(
active_only.status.success(),
"dep cycles active-only failed: {}",
active_only.stderr
);
let active_payload: Value = serde_json::from_str(&extract_json_payload(&active_only.stdout))
.expect("active cycles json");
assert_eq!(active_payload["scope"], "active");
assert_eq!(active_payload["include_closed"], false);
assert_eq!(active_payload["count"], 0);
assert_eq!(active_payload["active_count"], 0);
assert_eq!(active_payload["archived_closed_count"], 1);
assert_eq!(active_payload["total_count"], 1);
assert_eq!(active_payload["cycles"].as_array().unwrap().len(), 0);
assert!(active_payload.get("archived_closed_cycles").is_none());
let with_archive = run_br(
&workspace,
["dep", "cycles", "--json", "--include-closed"],
"cycles_include_closed",
);
assert!(
with_archive.status.success(),
"dep cycles --include-closed failed: {}",
with_archive.stderr
);
let archive_payload: Value = serde_json::from_str(&extract_json_payload(&with_archive.stdout))
.expect("include-closed cycles json");
assert_eq!(archive_payload["scope"], "active_and_archived");
assert_eq!(archive_payload["include_closed"], true);
assert_eq!(archive_payload["count"], 1);
assert_eq!(archive_payload["active_count"], 0);
assert_eq!(archive_payload["archived_closed_count"], 1);
assert_eq!(archive_payload["total_count"], 1);
assert_eq!(archive_payload["cycles"].as_array().unwrap().len(), 1);
assert_eq!(
archive_payload["archived_closed_cycles"]
.as_array()
.unwrap()
.len(),
1
);
info!(
"e2e_dep_cycles_default_hides_closed_archive_and_include_closed_exposes_it: assertions passed"
);
}
#[test]
fn e2e_relations_labels_comments() {
common::init_test_logging();
info!("e2e_relations_labels_comments: starting");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let parent = run_br(&workspace, ["create", "Parent issue"], "create_parent");
assert!(
parent.status.success(),
"parent create failed: {}",
parent.stderr
);
let parent_id = parse_created_id(&parent.stdout);
let child = run_br(&workspace, ["create", "Child issue"], "create_child");
assert!(
child.status.success(),
"child create failed: {}",
child.stderr
);
let child_id = parse_created_id(&child.stdout);
let parent_args = vec![
"update".to_string(),
child_id.clone(),
"--parent".to_string(),
parent_id,
];
let parent_update = run_br(&workspace, parent_args, "set_parent");
assert!(
parent_update.status.success(),
"parent update failed: {}",
parent_update.stderr
);
let label_args = vec![
"update".to_string(),
child_id.clone(),
"--add-label".to_string(),
"backend".to_string(),
];
let label_update = run_br(&workspace, label_args, "add_label");
assert!(
label_update.status.success(),
"label update failed: {}",
label_update.stderr
);
let list = run_br(
&workspace,
["list", "--label", "backend", "--json"],
"list_label",
);
assert!(list.status.success(), "list failed: {}", list.stderr);
let list_json = extract_issues_array(&list.stdout);
assert!(
list_json.iter().any(|item| item["id"] == child_id),
"labeled issue missing in list"
);
let comment_args = vec![
"comments".to_string(),
"add".to_string(),
child_id.clone(),
"First comment".to_string(),
];
let comment = run_br(&workspace, comment_args, "add_comment");
assert!(
comment.status.success(),
"comment add failed: {}",
comment.stderr
);
let list_comments = run_br(
&workspace,
["comments", "list", &child_id, "--json"],
"list_comments",
);
assert!(
list_comments.status.success(),
"comment list failed: {}",
list_comments.stderr
);
let comments_payload = extract_json_payload(&list_comments.stdout);
let comments_json: Vec<Value> = serde_json::from_str(&comments_payload).expect("comments json");
assert_eq!(comments_json.len(), 1);
assert_eq!(comments_json[0]["text"], "First comment");
info!("e2e_relations_labels_comments: assertions passed");
}
#[test]
fn e2e_label_add_updates_last_touched_context() {
common::init_test_logging();
info!("e2e_label_add_updates_last_touched_context: starting");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init_label_last_touched");
assert!(init.status.success(), "init failed: {}", init.stderr);
let target = run_br(
&workspace,
["create", "Label target"],
"create_label_target",
);
assert!(target.status.success(), "create failed: {}", target.stderr);
let target_id = parse_created_id(&target.stdout);
let other = run_br(&workspace, ["create", "Other issue"], "create_label_other");
assert!(other.status.success(), "create failed: {}", other.stderr);
let other_id = parse_created_id(&other.stdout);
let label_add = run_br(
&workspace,
["label", "add", &target_id, "triage", "--json"],
"label_add_last_touched",
);
assert!(
label_add.status.success(),
"label add failed: {}",
label_add.stderr
);
let update = run_br(
&workspace,
["update", "--title", "Label-touched target", "--json"],
"update_after_label_add_last_touched",
);
assert!(
update.status.success(),
"update without explicit id failed after label add: {}",
update.stderr
);
let show_target = run_br(
&workspace,
["show", &target_id, "--json"],
"show_label_target",
);
assert!(
show_target.status.success(),
"show target failed: {}",
show_target.stderr
);
let shown_target: Vec<Value> =
serde_json::from_str(&extract_json_payload(&show_target.stdout)).expect("target json");
assert_eq!(shown_target[0]["title"], "Label-touched target");
let show_other = run_br(
&workspace,
["show", &other_id, "--json"],
"show_label_other",
);
assert!(
show_other.status.success(),
"show other failed: {}",
show_other.stderr
);
let shown_other: Vec<Value> =
serde_json::from_str(&extract_json_payload(&show_other.stdout)).expect("other json");
assert_eq!(shown_other[0]["title"], "Other issue");
info!("e2e_label_add_updates_last_touched_context: assertions passed");
}
#[test]
fn e2e_comments_add_updates_last_touched_context() {
common::init_test_logging();
info!("e2e_comments_add_updates_last_touched_context: starting");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init_comments_last_touched");
assert!(init.status.success(), "init failed: {}", init.stderr);
let target = run_br(
&workspace,
["create", "Comment target"],
"create_comments_target",
);
assert!(target.status.success(), "create failed: {}", target.stderr);
let target_id = parse_created_id(&target.stdout);
let other = run_br(
&workspace,
["create", "Other comments issue"],
"create_comments_other",
);
assert!(other.status.success(), "create failed: {}", other.stderr);
let other_id = parse_created_id(&other.stdout);
let comment_add = run_br(
&workspace,
["comments", "add", &target_id, "Context anchor", "--json"],
"comments_add_last_touched",
);
assert!(
comment_add.status.success(),
"comments add failed: {}",
comment_add.stderr
);
let update = run_br(
&workspace,
["update", "--title", "Comment-touched target", "--json"],
"update_after_comments_add_last_touched",
);
assert!(
update.status.success(),
"update without explicit id failed after comments add: {}",
update.stderr
);
let show_target = run_br(
&workspace,
["show", &target_id, "--json"],
"show_comments_target",
);
assert!(
show_target.status.success(),
"show target failed: {}",
show_target.stderr
);
let shown_target: Vec<Value> =
serde_json::from_str(&extract_json_payload(&show_target.stdout)).expect("target json");
assert_eq!(shown_target[0]["title"], "Comment-touched target");
let show_other = run_br(
&workspace,
["show", &other_id, "--json"],
"show_comments_other",
);
assert!(
show_other.status.success(),
"show other failed: {}",
show_other.stderr
);
let shown_other: Vec<Value> =
serde_json::from_str(&extract_json_payload(&show_other.stdout)).expect("other json");
assert_eq!(shown_other[0]["title"], "Other comments issue");
info!("e2e_comments_add_updates_last_touched_context: assertions passed");
}
#[test]
fn e2e_dep_add_list_blocked_remove() {
common::init_test_logging();
info!("e2e_dep_add_list_blocked_remove: starting");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let blocking_issue = run_br(&workspace, ["create", "Blocker issue"], "create_blocker");
assert!(
blocking_issue.status.success(),
"blocker create failed: {}",
blocking_issue.stderr
);
let blocking_id = parse_created_id(&blocking_issue.stdout);
let blocked_issue = run_br(&workspace, ["create", "Blocked issue"], "create_blocked");
assert!(
blocked_issue.status.success(),
"blocked create failed: {}",
blocked_issue.stderr
);
let blocked_id = parse_created_id(&blocked_issue.stdout);
let dep_add = run_br(
&workspace,
["dep", "add", &blocked_id, &blocking_id, "--json"],
"dep_add",
);
assert!(
dep_add.status.success(),
"dep add failed: {}",
dep_add.stderr
);
let list = run_br(
&workspace,
["dep", "list", &blocked_id, "--json"],
"dep_list",
);
assert!(list.status.success(), "dep list failed: {}", list.stderr);
let list_payload = extract_json_payload(&list.stdout);
let list_json: Vec<Value> = serde_json::from_str(&list_payload).expect("dep list json");
assert!(
list_json
.iter()
.any(|item| item["issue_id"] == blocked_id && item["depends_on_id"] == blocking_id),
"dependency not listed"
);
let blocked_view = run_br(&workspace, ["blocked", "--json"], "blocked");
assert!(
blocked_view.status.success(),
"blocked failed: {}",
blocked_view.stderr
);
let blocked_payload = extract_json_payload(&blocked_view.stdout);
let blocked_json: Vec<Value> = serde_json::from_str(&blocked_payload).expect("blocked json");
assert!(
blocked_json.iter().any(|item| item["id"] == blocked_id),
"blocked issue missing from blocked list"
);
let dep_remove = run_br(
&workspace,
["dep", "remove", &blocked_id, &blocking_id, "--json"],
"dep_remove",
);
assert!(
dep_remove.status.success(),
"dep remove failed: {}",
dep_remove.stderr
);
let blocked_view = run_br(&workspace, ["blocked", "--json"], "blocked_after");
assert!(
blocked_view.status.success(),
"blocked after remove failed: {}",
blocked_view.stderr
);
let blocked_payload = extract_json_payload(&blocked_view.stdout);
let blocked_json: Vec<Value> = serde_json::from_str(&blocked_payload).expect("blocked json");
assert!(
!blocked_json.iter().any(|item| item["id"] == blocked_id),
"blocked issue still present after dep remove"
);
info!("e2e_dep_add_list_blocked_remove: assertions passed");
}
#[test]
fn e2e_dep_add_updates_last_touched_context() {
common::init_test_logging();
info!("e2e_dep_add_updates_last_touched_context: starting");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let blocker = run_br(&workspace, ["create", "Blocker"], "create_blocker");
assert!(
blocker.status.success(),
"create blocker failed: {}",
blocker.stderr
);
let blocker_id = parse_created_id(&blocker.stdout);
let blocked = run_br(&workspace, ["create", "Blocked"], "create_blocked");
assert!(
blocked.status.success(),
"create blocked failed: {}",
blocked.stderr
);
let blocked_id = parse_created_id(&blocked.stdout);
let dep_add = run_br(
&workspace,
["dep", "add", &blocked_id, &blocker_id, "--json"],
"dep_add_last_touched",
);
assert!(
dep_add.status.success(),
"dep add failed: {}",
dep_add.stderr
);
let update = run_br(
&workspace,
[
"update",
"--title",
"Blocked renamed via last touched",
"--json",
],
"update_last_touched_after_dep_add",
);
assert!(
update.status.success(),
"update without explicit id failed after dep add: {}",
update.stderr
);
let show = run_br(
&workspace,
["show", &blocked_id, "--json"],
"show_blocked_after_update",
);
assert!(show.status.success(), "show failed: {}", show.stderr);
let payload = extract_json_payload(&show.stdout);
let json: Value = serde_json::from_str(&payload).expect("show json");
assert_eq!(json[0]["title"], "Blocked renamed via last touched");
info!("e2e_dep_add_updates_last_touched_context: assertions passed");
}
#[test]
fn e2e_dep_remove_updates_last_touched_context() {
common::init_test_logging();
info!("e2e_dep_remove_updates_last_touched_context: starting");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let blocker = run_br(&workspace, ["create", "Blocker"], "create_blocker");
assert!(
blocker.status.success(),
"create blocker failed: {}",
blocker.stderr
);
let blocker_id = parse_created_id(&blocker.stdout);
let blocked = run_br(&workspace, ["create", "Blocked"], "create_blocked");
assert!(
blocked.status.success(),
"create blocked failed: {}",
blocked.stderr
);
let blocked_id = parse_created_id(&blocked.stdout);
let dep_add = run_br(
&workspace,
["dep", "add", &blocked_id, &blocker_id, "--json"],
"dep_add_before_remove",
);
assert!(
dep_add.status.success(),
"dep add failed: {}",
dep_add.stderr
);
let update_blocker = run_br(
&workspace,
[
"update",
&blocker_id,
"--title",
"Blocker renamed first",
"--json",
],
"update_blocker_first",
);
assert!(
update_blocker.status.success(),
"update blocker failed: {}",
update_blocker.stderr
);
let dep_remove = run_br(
&workspace,
["dep", "remove", &blocked_id, &blocker_id, "--json"],
"dep_remove_last_touched",
);
assert!(
dep_remove.status.success(),
"dep remove failed: {}",
dep_remove.stderr
);
let update = run_br(
&workspace,
[
"update",
"--title",
"Blocked renamed after dep remove",
"--json",
],
"update_last_touched_after_dep_remove",
);
assert!(
update.status.success(),
"update without explicit id failed after dep remove: {}",
update.stderr
);
let show = run_br(
&workspace,
["show", &blocked_id, "--json"],
"show_blocked_after_remove",
);
assert!(
show.status.success(),
"show blocked failed: {}",
show.stderr
);
let blocked_payload = extract_json_payload(&show.stdout);
let blocked_json: Value = serde_json::from_str(&blocked_payload).expect("show blocked json");
assert_eq!(blocked_json[0]["title"], "Blocked renamed after dep remove");
let show_blocker = run_br(
&workspace,
["show", &blocker_id, "--json"],
"show_blocker_final",
);
assert!(
show_blocker.status.success(),
"show blocker failed: {}",
show_blocker.stderr
);
let blocker_payload = extract_json_payload(&show_blocker.stdout);
let blocker_json: Value = serde_json::from_str(&blocker_payload).expect("show blocker json");
assert_eq!(blocker_json[0]["title"], "Blocker renamed first");
info!("e2e_dep_remove_updates_last_touched_context: assertions passed");
}
#[test]
#[allow(clippy::too_many_lines)]
fn e2e_dep_tree_external_nodes() {
common::init_test_logging();
info!("e2e_dep_tree_external_nodes: starting");
let workspace = BrWorkspace::new();
let external = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init_main");
assert!(init.status.success(), "init failed: {}", init.stderr);
let init_ext = run_br(&external, ["init"], "init_external");
assert!(
init_ext.status.success(),
"external init failed: {}",
init_ext.stderr
);
let external_config_path = external.root.join(".beads/config.yaml");
fs::write(&external_config_path, "issue_prefix: bd\n").expect("write ext config");
let config_path = workspace.root.join(".beads/config.yaml");
let external_path = external.root.display();
let config = format!("issue_prefix: bd\nexternal_projects:\n extproj: \"{external_path}\"\n");
fs::write(&config_path, config).expect("write config");
let issue = run_br(&workspace, ["create", "Main issue"], "create_main_issue");
assert!(issue.status.success(), "create failed: {}", issue.stderr);
let issue_id = parse_created_id(&issue.stdout);
let dep_add = run_br(
&workspace,
["dep", "add", &issue_id, "external:extproj:auth"],
"dep_add_external",
);
assert!(
dep_add.status.success(),
"dep add failed: {}",
dep_add.stderr
);
let tree_before = run_br(
&workspace,
["dep", "tree", &issue_id, "--json"],
"dep_tree_before",
);
assert!(
tree_before.status.success(),
"dep tree before failed: {}",
tree_before.stderr
);
let tree_payload = extract_json_payload(&tree_before.stdout);
let nodes: Vec<Value> = serde_json::from_str(&tree_payload).expect("tree json");
let external_node = nodes
.iter()
.find(|node| node["id"] == "external:extproj:auth")
.expect("external node");
assert_eq!(external_node["status"], "blocked");
assert!(
external_node["title"]
.as_str()
.unwrap_or("")
.starts_with('⏳'),
"external node should show pending marker"
);
let provider = run_br(&external, ["create", "Provide auth"], "ext_create");
assert!(
provider.status.success(),
"external create failed: {}",
provider.stderr
);
let provider_id = parse_created_id(&provider.stdout);
let label = run_br(
&external,
["update", &provider_id, "--add-label", "provides:auth"],
"ext_label",
);
assert!(
label.status.success(),
"external label failed: {}",
label.stderr
);
let close = run_br(&external, ["close", &provider_id], "ext_close");
assert!(
close.status.success(),
"external close failed: {}",
close.stderr
);
let tree_after = run_br(
&workspace,
["dep", "tree", &issue_id, "--json"],
"dep_tree_after",
);
assert!(
tree_after.status.success(),
"dep tree after failed: {}",
tree_after.stderr
);
let tree_payload = extract_json_payload(&tree_after.stdout);
let nodes: Vec<Value> = serde_json::from_str(&tree_payload).expect("tree json");
let external_node = nodes
.iter()
.find(|node| node["id"] == "external:extproj:auth")
.expect("external node");
assert_eq!(external_node["status"], "closed");
assert!(
external_node["title"]
.as_str()
.unwrap_or("")
.starts_with('✓'),
"external node should show satisfied marker"
);
info!("e2e_dep_tree_external_nodes: assertions passed");
}
#[test]
#[allow(clippy::too_many_lines)]
fn e2e_dep_list_external_nodes() {
common::init_test_logging();
info!("e2e_dep_list_external_nodes: starting");
let workspace = BrWorkspace::new();
let external = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init_main");
assert!(init.status.success(), "init failed: {}", init.stderr);
let init_ext = run_br(&external, ["init"], "init_external");
assert!(
init_ext.status.success(),
"external init failed: {}",
init_ext.stderr
);
let external_config_path = external.root.join(".beads/config.yaml");
fs::write(&external_config_path, "issue_prefix: bd\n").expect("write ext config");
let config_path = workspace.root.join(".beads/config.yaml");
let external_path = external.root.display();
let config = format!("issue_prefix: bd\nexternal_projects:\n extproj: \"{external_path}\"\n");
fs::write(&config_path, config).expect("write config");
let issue = run_br(&workspace, ["create", "Main issue"], "create_main_issue");
assert!(issue.status.success(), "create failed: {}", issue.stderr);
let issue_id = parse_created_id(&issue.stdout);
let dep_add = run_br(
&workspace,
["dep", "add", &issue_id, "external:extproj:auth"],
"dep_add_external",
);
assert!(
dep_add.status.success(),
"dep add failed: {}",
dep_add.stderr
);
let list_before = run_br(
&workspace,
["dep", "list", &issue_id, "--json"],
"dep_list_before",
);
assert!(
list_before.status.success(),
"dep list before failed: {}",
list_before.stderr
);
let list_payload = extract_json_payload(&list_before.stdout);
let list_json: Vec<Value> = serde_json::from_str(&list_payload).expect("dep list json");
let external_entry = list_json
.iter()
.find(|item| item["depends_on_id"] == "external:extproj:auth")
.expect("external dep entry");
assert_eq!(external_entry["status"], "blocked");
assert!(
external_entry["title"]
.as_str()
.unwrap_or("")
.starts_with('⏳'),
"external dep should show pending marker"
);
let provider = run_br(&external, ["create", "Provide auth"], "ext_create");
assert!(
provider.status.success(),
"external create failed: {}",
provider.stderr
);
let provider_id = parse_created_id(&provider.stdout);
let label = run_br(
&external,
["update", &provider_id, "--add-label", "provides:auth"],
"ext_label",
);
assert!(
label.status.success(),
"external label failed: {}",
label.stderr
);
let close = run_br(&external, ["close", &provider_id], "ext_close");
assert!(
close.status.success(),
"external close failed: {}",
close.stderr
);
let list_after = run_br(
&workspace,
["dep", "list", &issue_id, "--json"],
"dep_list_after",
);
assert!(
list_after.status.success(),
"dep list after failed: {}",
list_after.stderr
);
let list_payload = extract_json_payload(&list_after.stdout);
let list_json: Vec<Value> = serde_json::from_str(&list_payload).expect("dep list json");
let external_entry = list_json
.iter()
.find(|item| item["depends_on_id"] == "external:extproj:auth")
.expect("external dep entry");
assert_eq!(external_entry["status"], "closed");
assert!(
external_entry["title"]
.as_str()
.unwrap_or("")
.starts_with('✓'),
"external dep should show satisfied marker"
);
info!("e2e_dep_list_external_nodes: assertions passed");
}
#[test]
fn e2e_close_suggest_next_unblocks() {
common::init_test_logging();
info!("e2e_close_suggest_next_unblocks: starting");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let blocker = run_br(&workspace, ["create", "Blocker issue"], "create_blocker");
assert!(
blocker.status.success(),
"blocker create failed: {}",
blocker.stderr
);
let blocker_id = parse_created_id(&blocker.stdout);
let blocked = run_br(&workspace, ["create", "Blocked issue"], "create_blocked");
assert!(
blocked.status.success(),
"blocked create failed: {}",
blocked.stderr
);
let blocked_id = parse_created_id(&blocked.stdout);
let dep_add = run_br(
&workspace,
["dep", "add", &blocked_id, &blocker_id],
"dep_add",
);
assert!(
dep_add.status.success(),
"dep add failed: {}",
dep_add.stderr
);
let close = run_br(
&workspace,
["close", &blocker_id, "--suggest-next", "--json"],
"close_suggest_next",
);
assert!(close.status.success(), "close failed: {}", close.stderr);
let payload = extract_json_payload(&close.stdout);
let close_json: serde_json::Value = serde_json::from_str(&payload).expect("close json");
let unblocked = close_json["unblocked"]
.as_array()
.cloned()
.unwrap_or_default();
assert!(
unblocked.iter().any(|item| item["id"] == blocked_id),
"blocked issue not reported as unblocked"
);
info!("e2e_close_suggest_next_unblocks: assertions passed");
}
#[test]
fn e2e_close_blocked_requires_force() {
common::init_test_logging();
info!("e2e_close_blocked_requires_force: starting");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let blocker = run_br(&workspace, ["create", "Blocker issue"], "create_blocker");
assert!(
blocker.status.success(),
"blocker create failed: {}",
blocker.stderr
);
let blocker_id = parse_created_id(&blocker.stdout);
let blocked = run_br(&workspace, ["create", "Blocked issue"], "create_blocked");
assert!(
blocked.status.success(),
"blocked create failed: {}",
blocked.stderr
);
let blocked_id = parse_created_id(&blocked.stdout);
let dep_add = run_br(
&workspace,
["dep", "add", &blocked_id, &blocker_id],
"dep_add",
);
assert!(
dep_add.status.success(),
"dep add failed: {}",
dep_add.stderr
);
let close_skip = run_br(
&workspace,
["close", &blocked_id, "--json"],
"close_blocked_skip",
);
assert!(
!close_skip.status.success(),
"close blocked should fail with nothing-to-do: {}",
close_skip.stdout
);
let payload = extract_json_payload(&close_skip.stdout);
let close_json: Value = serde_json::from_str(&payload).expect("close json");
let closed = close_json["closed"].as_array().cloned().unwrap_or_default();
let skipped = close_json["skipped"]
.as_array()
.cloned()
.unwrap_or_default();
assert!(
closed.is_empty(),
"blocked issue should not close without --force"
);
assert_eq!(
skipped.len(),
1,
"blocked close should report one skipped issue"
);
assert_eq!(skipped[0]["id"], blocked_id);
assert!(
skipped[0]["reason"]
.as_str()
.is_some_and(|reason| reason.contains("blocked by:")),
"blocked close should explain the dependency blocker"
);
let show = run_br(
&workspace,
["show", &blocked_id, "--json"],
"show_blocked_after_skip",
);
let payload = extract_json_payload(&show.stdout);
let issues: Value = serde_json::from_str(&payload).expect("show json");
assert_eq!(issues[0]["status"].as_str().unwrap(), "open");
let close_force = run_br(
&workspace,
["close", &blocked_id, "--force", "--json"],
"close_blocked_force",
);
assert!(
close_force.status.success(),
"close force failed: {}",
close_force.stderr
);
let payload = extract_json_payload(&close_force.stdout);
let close_json: Value = serde_json::from_str(&payload).expect("close json");
let closed = close_json.as_array().cloned().unwrap_or_default();
assert!(
closed.iter().any(|item| item["id"] == blocked_id),
"blocked issue not closed with --force"
);
info!("e2e_close_blocked_requires_force: assertions passed");
}
#[test]
fn e2e_close_json_reports_closed_and_skipped_in_partial_batch() {
common::init_test_logging();
info!("e2e_close_json_reports_closed_and_skipped_in_partial_batch: starting");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let blocker = run_br(&workspace, ["create", "Blocker issue"], "create_blocker");
assert!(
blocker.status.success(),
"blocker create failed: {}",
blocker.stderr
);
let blocker_id = parse_created_id(&blocker.stdout);
let blocked = run_br(&workspace, ["create", "Blocked issue"], "create_blocked");
assert!(
blocked.status.success(),
"blocked create failed: {}",
blocked.stderr
);
let blocked_id = parse_created_id(&blocked.stdout);
let independent = run_br(
&workspace,
["create", "Independent issue"],
"create_independent",
);
assert!(
independent.status.success(),
"independent create failed: {}",
independent.stderr
);
let independent_id = parse_created_id(&independent.stdout);
let dep_add = run_br(
&workspace,
["dep", "add", &blocked_id, &blocker_id],
"dep_add",
);
assert!(
dep_add.status.success(),
"dep add failed: {}",
dep_add.stderr
);
let close = run_br(
&workspace,
["close", &blocked_id, &independent_id, "--json"],
"close_partial_batch",
);
assert!(
close.status.success(),
"partial close should succeed: {}",
close.stderr
);
let payload = extract_json_payload(&close.stdout);
let close_json: Value = serde_json::from_str(&payload).expect("close json");
let closed = close_json["closed"].as_array().cloned().unwrap_or_default();
let skipped = close_json["skipped"]
.as_array()
.cloned()
.unwrap_or_default();
assert_eq!(
closed.len(),
1,
"partial close should report one closed issue"
);
assert_eq!(closed[0]["id"], independent_id);
assert_eq!(
skipped.len(),
1,
"partial close should report one skipped issue"
);
assert_eq!(skipped[0]["id"], blocked_id);
assert!(
skipped[0]["reason"]
.as_str()
.is_some_and(|reason| reason.contains("blocked by:")),
"partial close should preserve skipped blocker details"
);
info!("e2e_close_json_reports_closed_and_skipped_in_partial_batch: assertions passed");
}
#[test]
fn e2e_close_honors_env_json_mode() {
common::init_test_logging();
info!("e2e_close_honors_env_json_mode: starting");
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", "Close via env json", "--json"],
"create_env_json_close",
);
assert!(create.status.success(), "create failed: {}", create.stderr);
let created: Value =
serde_json::from_str(&extract_json_payload(&create.stdout)).expect("create json");
let issue_id = created["id"].as_str().expect("issue id");
let close = common::cli::run_br_with_env(
&workspace,
["close", issue_id],
[("BR_OUTPUT_FORMAT", "json")],
"close_env_json",
);
assert!(
close.status.success(),
"close with env json failed: {}",
close.stderr
);
let payload = extract_json_payload(&close.stdout);
let close_json: Value = serde_json::from_str(&payload).expect("close json");
let closed = close_json.as_array().cloned().unwrap_or_default();
assert_eq!(closed.len(), 1);
assert_eq!(closed[0]["id"], issue_id);
assert_eq!(closed[0]["status"], "closed");
info!("e2e_close_honors_env_json_mode: assertions passed");
}