mod common;
use common::cli::{BrWorkspace, extract_issues_array, extract_json_payload, run_br};
use serde_json::Value;
use std::collections::HashSet;
#[test]
fn q_creates_issue_returns_id_only() {
let _log = common::test_log("q_creates_issue_returns_id_only");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let quick = run_br(&workspace, ["q", "Test quick capture"], "quick");
assert!(quick.status.success(), "q failed: {}", quick.stderr);
let output = quick.stdout.trim();
assert_eq!(output.lines().count(), 1, "q should output only one line");
assert!(
output.contains('-'),
"output should be an issue ID (prefix-hash format), got: {output}"
);
assert!(
!output.to_lowercase().contains("created"),
"q should not include 'Created' text"
);
}
#[test]
fn q_with_type_flag() {
let _log = common::test_log("q_with_type_flag");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let quick = run_br(
&workspace,
["q", "Bug report", "--type", "bug"],
"quick_type",
);
assert!(
quick.status.success(),
"q with --type failed: {}",
quick.stderr
);
let id = quick.stdout.trim();
let show = run_br(&workspace, ["show", id, "--json"], "show");
assert!(show.status.success(), "show failed: {}", show.stderr);
let payload = extract_json_payload(&show.stdout);
let json: Vec<Value> = serde_json::from_str(&payload).expect("parse json");
assert_eq!(json[0]["issue_type"], "bug", "issue type should be 'bug'");
}
#[test]
fn q_with_priority_flag() {
let _log = common::test_log("q_with_priority_flag");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let quick = run_br(
&workspace,
["q", "High priority issue", "-p", "1"],
"quick_priority",
);
assert!(quick.status.success(), "q with -p failed: {}", quick.stderr);
let id = quick.stdout.trim();
let show = run_br(&workspace, ["show", id, "--json"], "show");
assert!(show.status.success(), "show failed: {}", show.stderr);
let payload = extract_json_payload(&show.stdout);
let json: Vec<Value> = serde_json::from_str(&payload).expect("parse json");
assert_eq!(json[0]["priority"], 1, "priority should be 1");
}
#[test]
fn q_with_all_flags() {
let _log = common::test_log("q_with_all_flags");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let quick = run_br(
&workspace,
[
"q",
"Critical bug",
"-t",
"bug",
"-p",
"0",
"-l",
"urgent",
"-l",
"regression",
],
"quick_all",
);
assert!(
quick.status.success(),
"q with all flags failed: {}",
quick.stderr
);
let id = quick.stdout.trim();
let show = run_br(&workspace, ["show", id, "--json"], "show");
assert!(show.status.success(), "show failed: {}", show.stderr);
let payload = extract_json_payload(&show.stdout);
let json: Vec<Value> = serde_json::from_str(&payload).expect("parse json");
assert_eq!(json[0]["issue_type"], "bug");
assert_eq!(json[0]["priority"], 0);
let labels = json[0]["labels"]
.as_array()
.expect("labels should be array");
let label_names: Vec<&str> = labels.iter().filter_map(|l| l.as_str()).collect();
assert!(
label_names.contains(&"urgent"),
"should have 'urgent' label"
);
assert!(
label_names.contains(&"regression"),
"should have 'regression' label"
);
}
#[test]
fn q_with_labels() {
let _log = common::test_log("q_with_labels");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let quick = run_br(
&workspace,
["q", "Labeled issue", "-l", "frontend,backend,api"],
"quick_labels",
);
assert!(
quick.status.success(),
"q with labels failed: {}",
quick.stderr
);
let id = quick.stdout.trim();
let show = run_br(&workspace, ["show", id, "--json"], "show");
assert!(show.status.success(), "show failed: {}", show.stderr);
let payload = extract_json_payload(&show.stdout);
let json: Vec<Value> = serde_json::from_str(&payload).expect("parse json");
let labels = json[0]["labels"]
.as_array()
.expect("labels should be array");
assert_eq!(labels.len(), 3, "should have 3 labels");
let label_names: Vec<&str> = labels.iter().filter_map(|l| l.as_str()).collect();
assert!(label_names.contains(&"frontend"));
assert!(label_names.contains(&"backend"));
assert!(label_names.contains(&"api"));
}
#[test]
fn q_updates_last_touched_context() {
let _log = common::test_log("q_updates_last_touched_context");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init_q_last_touched");
assert!(init.status.success(), "init failed: {}", init.stderr);
let quick = run_br(
&workspace,
["q", "Quick capture updates last touched"],
"quick_last_touched",
);
assert!(quick.status.success(), "q failed: {}", quick.stderr);
let created_id = quick.stdout.trim().to_string();
assert!(!created_id.is_empty(), "missing quick-capture id");
let update = run_br(
&workspace,
["update", "--status", "in_progress"],
"update_last_touched_after_q",
);
assert!(update.status.success(), "update failed: {}", update.stderr);
let show = run_br(
&workspace,
["show", &created_id, "--json"],
"show_last_touched_after_q",
);
assert!(show.status.success(), "show failed: {}", show.stderr);
let payload = extract_json_payload(&show.stdout);
let json: Vec<Value> = serde_json::from_str(&payload).expect("parse json");
assert_eq!(json[0]["status"], "in_progress");
}
#[test]
fn q_multiple_words_title() {
let _log = common::test_log("q_multiple_words_title");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let quick = run_br(
&workspace,
["q", "This", "is", "a", "multi", "word", "title"],
"quick_multiword",
);
assert!(
quick.status.success(),
"q with multiple words failed: {}",
quick.stderr
);
let id = quick.stdout.trim();
let show = run_br(&workspace, ["show", id, "--json"], "show");
assert!(show.status.success(), "show failed: {}", show.stderr);
let payload = extract_json_payload(&show.stdout);
let json: Vec<Value> = serde_json::from_str(&payload).expect("parse json");
assert_eq!(
json[0]["title"], "This is a multi word title",
"title should be joined words"
);
}
#[test]
fn q_output_is_valid_id() {
let _log = common::test_log("q_output_is_valid_id");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let quick = run_br(&workspace, ["q", "ID format test"], "quick_id");
assert!(quick.status.success(), "q failed: {}", quick.stderr);
let id = quick.stdout.trim();
assert!(id.contains('-'), "ID should contain hyphen separator");
let parts: Vec<&str> = id.split('-').collect();
assert!(parts.len() >= 2, "ID should have prefix and hash parts");
let hash_part = parts[1..].join("-"); assert!(
hash_part.chars().all(|c| c.is_ascii_alphanumeric()),
"hash part should be alphanumeric: {hash_part}"
);
}
#[test]
fn q_issue_appears_in_list() {
let _log = common::test_log("q_issue_appears_in_list");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let quick = run_br(&workspace, ["q", "Listable issue"], "quick_list");
assert!(quick.status.success(), "q failed: {}", quick.stderr);
let id = quick.stdout.trim();
let list = run_br(&workspace, ["list", "--json"], "list");
assert!(list.status.success(), "list failed: {}", list.stderr);
let json = extract_issues_array(&list.stdout);
let found = json.iter().any(|issue| issue["id"] == id);
assert!(found, "issue {id} should appear in list output");
}
#[test]
fn q_parent_accepts_short_parent_id() {
let _log = common::test_log("q_parent_accepts_short_parent_id");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let parent_create = run_br(&workspace, ["q", "Parent issue"], "q_parent");
assert!(
parent_create.status.success(),
"parent q failed: {}",
parent_create.stderr
);
let parent_id = parent_create.stdout.trim().to_string();
let short_parent_id = parent_id
.split_once('-')
.map(|(_, hash)| hash)
.expect("parent id should contain hash segment");
let child_create = run_br(
&workspace,
["q", "Child issue", "--parent", short_parent_id],
"q_child_short_parent",
);
assert!(
child_create.status.success(),
"q with short parent id failed: {}",
child_create.stderr
);
let child_id = child_create.stdout.trim();
assert!(
child_id.starts_with(&format!("{parent_id}.")),
"child id should be generated under the resolved parent: {child_id}"
);
let show = run_br(&workspace, ["show", child_id, "--json"], "show_child");
assert!(show.status.success(), "show failed: {}", show.stderr);
let payload = extract_json_payload(&show.stdout);
let json: Vec<Value> = serde_json::from_str(&payload).expect("parse json");
let dependencies = json[0]["dependencies"]
.as_array()
.expect("dependencies should be an array");
assert!(
dependencies
.iter()
.any(|dep| { dep["id"] == parent_id && dep["dependency_type"] == "parent-child" }),
"child should reference the resolved parent dependency: {dependencies:?}"
);
}
#[test]
fn q_without_init_fails() {
let _log = common::test_log("q_without_init_fails");
let workspace = BrWorkspace::new();
let quick = run_br(&workspace, ["q", "No init"], "quick_no_init");
assert!(
!quick.status.success(),
"q should fail without init, but succeeded"
);
assert!(
quick.stderr.contains("not initialized")
|| quick.stderr.contains("Not initialized")
|| quick.stderr.contains(".beads")
|| quick.stderr.contains("init"),
"error should mention initialization: {}",
quick.stderr
);
}
#[test]
fn q_empty_title_fails() {
let _log = common::test_log("q_empty_title_fails");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let quick = run_br(&workspace, ["q", ""], "quick_empty");
assert!(
!quick.status.success(),
"q should fail with empty title, but succeeded"
);
assert!(
quick.stderr.to_lowercase().contains("empty")
|| quick.stderr.to_lowercase().contains("title")
|| quick.stderr.to_lowercase().contains("cannot be empty"),
"error should mention empty title: {}",
quick.stderr
);
}
#[test]
fn q_with_custom_type_succeeds() {
let _log = common::test_log("q_with_custom_type_succeeds");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let quick = run_br(
&workspace,
["q", "Custom type issue", "--type", "my_custom_type"],
"quick_custom_type",
);
assert!(
quick.status.success(),
"q with custom type should succeed: {}",
quick.stderr
);
let list = run_br(&workspace, ["list", "--json"], "list_check");
assert!(list.status.success(), "list failed: {}", list.stderr);
assert!(
list.stdout.contains("my_custom_type"),
"custom type should be preserved in the issue"
);
}
#[test]
fn q_invalid_priority_fails() {
let _log = common::test_log("q_invalid_priority_fails");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let quick = run_br(
&workspace,
["q", "Bad priority", "-p", "99"],
"quick_bad_priority",
);
assert!(
!quick.status.success(),
"q should fail with priority 99, but succeeded"
);
assert!(
quick.stderr.to_lowercase().contains("priority")
|| quick.stderr.to_lowercase().contains("invalid")
|| quick.stderr.contains("99"),
"error should mention invalid priority: {}",
quick.stderr
);
}
#[test]
fn q_output_usable_in_pipeline() {
let _log = common::test_log("q_output_usable_in_pipeline");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let quick = run_br(&workspace, ["q", "Pipeline test"], "quick_pipeline");
assert!(quick.status.success(), "q failed: {}", quick.stderr);
let id = quick.stdout.trim();
let show = run_br(&workspace, ["show", id], "show_pipeline");
assert!(
show.status.success(),
"show should succeed with q output: {}",
show.stderr
);
assert!(
show.stdout.contains("Pipeline test"),
"show should display the issue title"
);
let update = run_br(
&workspace,
["update", id, "--status", "in_progress"],
"update_pipeline",
);
assert!(
update.status.success(),
"update should succeed with q output: {}",
update.stderr
);
}
#[test]
fn q_multiple_creates_unique_ids() {
let _log = common::test_log("q_multiple_creates_unique_ids");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let mut ids: HashSet<String> = HashSet::new();
for i in 0..10 {
let quick = run_br(
&workspace,
["q", &format!("Rapid issue {i}")],
&format!("quick_{i}"),
);
assert!(quick.status.success(), "q #{i} failed: {}", quick.stderr);
let id = quick.stdout.trim().to_string();
assert!(!id.is_empty(), "ID #{i} should not be empty");
let is_new = ids.insert(id.clone());
assert!(is_new, "ID {id} should be unique, but was duplicate");
}
assert_eq!(ids.len(), 10, "should have 10 unique IDs");
let list = run_br(&workspace, ["list", "--json"], "list_all");
assert!(list.status.success(), "list failed: {}", list.stderr);
let json = extract_issues_array(&list.stdout);
assert_eq!(json.len(), 10, "should have 10 issues");
}
#[test]
fn q_silent_mode_stderr() {
let _log = common::test_log("q_silent_mode_stderr");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let quick = run_br(&workspace, ["q", "Silent test"], "quick_silent");
assert!(quick.status.success(), "q failed: {}", quick.stderr);
let stderr_lines: Vec<&str> = quick
.stderr
.lines()
.filter(|line| {
!line.contains("DEBUG")
&& !line.contains("INFO")
&& !line.contains("WARN")
&& !line.contains("TRACE")
&& !line.trim().is_empty()
})
.collect();
assert!(
stderr_lines.is_empty(),
"stderr should be empty on success (excluding logs), got: {stderr_lines:?}"
);
}
#[test]
fn q_with_p_prefix_priority() {
let _log = common::test_log("q_with_p_prefix_priority");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let quick = run_br(
&workspace,
["q", "P-format priority", "-p", "P0"],
"quick_p_format",
);
assert!(
quick.status.success(),
"q with P0 format failed: {}",
quick.stderr
);
let id = quick.stdout.trim();
let show = run_br(&workspace, ["show", id, "--json"], "show");
assert!(show.status.success(), "show failed: {}", show.stderr);
let payload = extract_json_payload(&show.stdout);
let json: Vec<Value> = serde_json::from_str(&payload).expect("parse json");
assert_eq!(json[0]["priority"], 0, "P0 should map to priority 0");
}
#[test]
fn q_special_characters_in_title() {
let _log = common::test_log("q_special_characters_in_title");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let title = "Fix bug: can't parse \"quotes\" & <special> chars!";
let quick = run_br(&workspace, ["q", title], "quick_special");
assert!(
quick.status.success(),
"q with special chars failed: {}",
quick.stderr
);
let id = quick.stdout.trim();
let show = run_br(&workspace, ["show", id, "--json"], "show");
assert!(show.status.success(), "show failed: {}", show.stderr);
let payload = extract_json_payload(&show.stdout);
let json: Vec<Value> = serde_json::from_str(&payload).expect("parse json");
assert_eq!(
json[0]["title"], title,
"special characters should be preserved"
);
}
#[test]
fn q_default_values() {
let _log = common::test_log("q_default_values");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let quick = run_br(&workspace, ["q", "Default values test"], "quick_defaults");
assert!(quick.status.success(), "q failed: {}", quick.stderr);
let id = quick.stdout.trim();
let show = run_br(&workspace, ["show", id, "--json"], "show");
assert!(show.status.success(), "show failed: {}", show.stderr);
let payload = extract_json_payload(&show.stdout);
let json: Vec<Value> = serde_json::from_str(&payload).expect("parse json");
assert_eq!(json[0]["issue_type"], "task", "default type should be task");
assert_eq!(json[0]["priority"], 2, "default priority should be 2");
assert_eq!(json[0]["status"], "open", "status should be open");
}
#[test]
fn q_status_is_always_open() {
let _log = common::test_log("q_status_is_always_open");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let ids: Vec<String> = vec![
run_br(&workspace, ["q", "Issue 1", "-t", "bug"], "q1"),
run_br(&workspace, ["q", "Issue 2", "-p", "0"], "q2"),
run_br(
&workspace,
["q", "Issue 3", "-t", "feature", "-p", "1"],
"q3",
),
]
.into_iter()
.filter(|r| r.status.success())
.map(|r| r.stdout.trim().to_string())
.collect();
assert_eq!(ids.len(), 3, "all creates should succeed");
for id in ids {
let show = run_br(&workspace, ["show", &id, "--json"], &format!("show_{id}"));
assert!(show.status.success(), "show failed: {}", show.stderr);
let payload = extract_json_payload(&show.stdout);
let json: Vec<Value> = serde_json::from_str(&payload).expect("parse json");
assert_eq!(
json[0]["status"], "open",
"issue {id} status should be open"
);
}
}