#![allow(clippy::all, clippy::pedantic, clippy::nursery, dead_code)]
mod common;
use assert_cmd::Command;
use chrono::Utc;
use common::cli::extract_json_payload;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_yml::Value as YamlValue;
use sha2::{Digest, Sha256};
use std::collections::HashSet;
use std::ffi::OsStr;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant, SystemTime};
use tempfile::TempDir;
use tracing::info;
fn get_bd_binary() -> String {
std::env::var("BD_BINARY").unwrap_or_else(|_| "bd".to_string())
}
pub fn bd_available() -> bool {
let bd_bin = get_bd_binary();
std::process::Command::new(&bd_bin)
.arg("version")
.output()
.is_ok_and(|o| {
if !o.status.success() {
return false;
}
let stdout = String::from_utf8_lossy(&o.stdout);
let Some(first_token) = stdout.split_whitespace().next() else {
return false;
};
match first_token.to_ascii_lowercase().as_str() {
"bd" | "beads" => true,
"br" => false,
_ => false,
}
})
}
macro_rules! skip_if_no_bd {
() => {
if !bd_available() {
eprintln!(
"Skipping test: 'bd' binary missing or aliased to br. \
Set BD_BINARY to a Go bd path for conformance runs."
);
return;
}
};
}
#[derive(Debug)]
pub struct CmdOutput {
pub stdout: String,
pub stderr: String,
pub status: std::process::ExitStatus,
pub duration: Duration,
}
pub struct ConformanceWorkspace {
pub temp_dir: TempDir,
pub br_root: PathBuf,
pub bd_root: PathBuf,
pub log_dir: PathBuf,
}
impl ConformanceWorkspace {
pub fn new() -> Self {
let temp_dir = TempDir::new().expect("create temp dir");
let root = temp_dir.path().to_path_buf();
let br_root = root.join("br_workspace");
let bd_root = root.join("bd_workspace");
let log_dir = root.join("logs");
fs::create_dir_all(&br_root).expect("create br workspace");
fs::create_dir_all(&bd_root).expect("create bd workspace");
fs::create_dir_all(&log_dir).expect("create log dir");
Self {
temp_dir,
br_root,
bd_root,
log_dir,
}
}
pub fn init_both(&self) -> (CmdOutput, CmdOutput) {
let br_out = self.run_br(["init"], "init");
let bd_out = self.run_bd(["init"], "init");
(br_out, bd_out)
}
pub fn run_br<I, S>(&self, args: I, label: &str) -> CmdOutput
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
run_br_cmd(&self.br_root, &self.log_dir, args, &format!("br_{label}"))
}
pub fn run_br_in_bd_env<I, S>(&self, args: I, label: &str) -> CmdOutput
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
run_br_cmd(
&self.bd_root,
&self.log_dir,
args,
&format!("br_in_bd_{label}"),
)
}
pub fn run_bd<I, S>(&self, args: I, label: &str) -> CmdOutput
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
run_bd_cmd(&self.bd_root, &self.log_dir, args, &format!("bd_{label}"))
}
}
fn run_br_cmd<I, S>(cwd: &PathBuf, log_dir: &PathBuf, args: I, label: &str) -> CmdOutput
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("br"));
cmd.current_dir(cwd);
cmd.args(args);
cmd.env("NO_COLOR", "1");
cmd.env("RUST_LOG", "beads_rust=debug");
cmd.env("RUST_BACKTRACE", "1");
cmd.env("HOME", cwd);
run_and_log(cmd, cwd, log_dir, label)
}
fn run_bd_cmd<I, S>(cwd: &PathBuf, log_dir: &PathBuf, args: I, label: &str) -> CmdOutput
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let bd_bin = get_bd_binary();
run_cmd_system(&bd_bin, cwd, log_dir, args, label)
}
fn run_cmd_system<I, S>(
binary: &str,
cwd: &PathBuf,
log_dir: &PathBuf,
args: I,
label: &str,
) -> CmdOutput
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut cmd = std::process::Command::new(binary);
cmd.current_dir(cwd);
cmd.args(args);
cmd.env("NO_COLOR", "1");
cmd.env("HOME", cwd);
cmd.env("BEADS_DIR", cwd.join(".beads"));
let start = Instant::now();
let output = cmd.output().expect(&format!("run {binary}"));
let duration = start.elapsed();
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let log_path = log_dir.join(format!("{label}.log"));
let timestamp = SystemTime::now();
let log_body = format!(
"label: {label}\nbinary: {binary}\nstarted: {:?}\nduration: {:?}\nstatus: {}\ncwd: {}\n\nstdout:\n{}\n\nstderr:\n{}\n",
timestamp,
duration,
output.status,
cwd.display(),
stdout,
stderr
);
fs::write(&log_path, log_body).expect("write log");
let entry = RunLogEntry {
timestamp: Utc::now().to_rfc3339(),
label: label.to_string(),
binary: binary.to_string(),
args: cmd
.get_args()
.map(|arg| arg.to_string_lossy().to_string())
.collect(),
cwd: cwd.display().to_string(),
status_code: output.status.code().unwrap_or(-1),
success: output.status.success(),
duration_ms: duration.as_millis(),
stdout_len: stdout.len(),
stderr_len: stderr.len(),
stdout_sha256: Some(sha256_hex(&stdout)),
stderr_sha256: Some(sha256_hex(&stderr)),
log_path: log_path.display().to_string(),
};
record_run(log_dir, entry, &stdout, &stderr, cwd);
CmdOutput {
stdout,
stderr,
status: output.status,
duration,
}
}
fn run_and_log(mut cmd: Command, cwd: &PathBuf, log_dir: &PathBuf, label: &str) -> CmdOutput {
let start = Instant::now();
let output = cmd.output().expect("run command");
let duration = start.elapsed();
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let log_path = log_dir.join(format!("{label}.log"));
let timestamp = SystemTime::now();
let log_body = format!(
"label: {label}\nstarted: {:?}\nduration: {:?}\nstatus: {}\nargs: {:?}\ncwd: {}\n\nstdout:\n{}\n\nstderr:\n{}\n",
timestamp,
duration,
output.status,
cmd.get_args().collect::<Vec<_>>(),
cwd.display(),
stdout,
stderr
);
fs::write(&log_path, log_body).expect("write log");
let entry = RunLogEntry {
timestamp: Utc::now().to_rfc3339(),
label: label.to_string(),
binary: "br".to_string(),
args: cmd
.get_args()
.map(|arg| arg.to_string_lossy().to_string())
.collect(),
cwd: cwd.display().to_string(),
status_code: output.status.code().unwrap_or(-1),
success: output.status.success(),
duration_ms: duration.as_millis(),
stdout_len: stdout.len(),
stderr_len: stderr.len(),
stdout_sha256: Some(sha256_hex(&stdout)),
stderr_sha256: Some(sha256_hex(&stderr)),
log_path: log_path.display().to_string(),
};
record_run(log_dir, entry, &stdout, &stderr, cwd);
CmdOutput {
stdout,
stderr,
status: output.status,
duration,
}
}
#[derive(Debug, Clone)]
pub enum CompareMode {
ExactJson,
NormalizedJson,
ContainsFields(Vec<String>),
ExitCodeOnly,
ArrayUnordered,
FieldsExcluded(Vec<String>),
StructureOnly,
}
#[derive(Debug, Clone)]
struct LogConfig {
json_logs: bool,
junit: bool,
summary: bool,
failure_context: bool,
}
impl LogConfig {
fn from_env() -> Self {
Self {
json_logs: env_flag("CONFORMANCE_JSON_LOGS"),
junit: env_flag("CONFORMANCE_JUNIT_XML"),
summary: env_flag("CONFORMANCE_SUMMARY"),
failure_context: env_flag("CONFORMANCE_FAILURE_CONTEXT"),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct RunLogEntry {
timestamp: String,
label: String,
binary: String,
args: Vec<String>,
cwd: String,
status_code: i32,
success: bool,
duration_ms: u128,
stdout_len: usize,
stderr_len: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
stdout_sha256: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
stderr_sha256: Option<String>,
log_path: String,
}
#[derive(Debug, Serialize, Deserialize, Default)]
struct SummaryStats {
runs: u64,
failures: u64,
total_ms: u128,
}
#[derive(Debug, Serialize, Deserialize, Default)]
struct SummaryReport {
generated_at: String,
total_runs: u64,
total_failures: u64,
by_binary: std::collections::HashMap<String, SummaryStats>,
by_label: std::collections::HashMap<String, SummaryStats>,
comparisons: std::collections::HashMap<String, ComparisonStats>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
struct ComparisonStats {
br_runs: u64,
bd_runs: u64,
br_total_ms: u128,
bd_total_ms: u128,
speedup_bd_over_br: Option<f64>,
}
static LOG_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
fn log_mutex() -> &'static Mutex<()> {
LOG_MUTEX.get_or_init(|| Mutex::new(()))
}
fn env_flag(name: &str) -> bool {
match std::env::var(name) {
Ok(value) => matches!(
value.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
),
Err(_) => false,
}
}
fn sha256_hex(input: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
beads_rust::util::hex_encode(&hasher.finalize())
}
#[test]
fn conformance_content_hash_matches_go_bd_fixture() {
use beads_rust::model::{IssueType, Priority, Status};
let hash = beads_rust::util::content_hash_from_parts(
"Fix authentication bug",
Some("Users are getting logged out unexpectedly"),
Some("Use token refresh"),
Some("Session survives refresh"),
Some("Check logs"),
&Status::InProgress,
&Priority::HIGH,
&IssueType::Bug,
Some("bob"),
Some("alice"),
Some("pane6"),
Some("github:org/repo#123"),
Some("github"),
true,
true,
);
assert_eq!(
hash, "ad16572b6b2e1a60df3520b2b91d181246707390f487a73caa24082dcd24e00e",
"content_hash must match the Go bd ComputeContentHash fixture"
);
}
fn collect_dir_listing(path: &PathBuf) -> Vec<String> {
let mut entries = Vec::new();
if let Ok(read_dir) = fs::read_dir(path) {
for entry in read_dir.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if let Ok(meta) = entry.metadata() {
if meta.is_dir() {
entries.push(format!("{name}/"));
} else {
entries.push(format!("{name} ({:?} bytes)", meta.len()));
}
} else {
entries.push(name);
}
}
}
entries.sort();
entries
}
fn append_run_entry(log_dir: &PathBuf, entry: &RunLogEntry) {
let log_path = log_dir.join("conformance_runs.jsonl");
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.expect("open conformance_runs.jsonl");
let json = serde_json::to_string(entry).expect("serialize run entry");
writeln!(file, "{json}").expect("append run entry");
}
fn read_run_entries(log_dir: &PathBuf) -> Vec<RunLogEntry> {
let log_path = log_dir.join("conformance_runs.jsonl");
let Ok(contents) = fs::read_to_string(&log_path) else {
return Vec::new();
};
contents
.lines()
.filter_map(|line| serde_json::from_str::<RunLogEntry>(line).ok())
.collect()
}
fn update_summary(log_dir: &PathBuf, entries: &[RunLogEntry]) {
let mut report = SummaryReport::default();
report.generated_at = chrono::Utc::now().to_rfc3339();
for entry in entries {
report.total_runs += 1;
if !entry.success {
report.total_failures += 1;
}
let by_binary = report
.by_binary
.entry(entry.binary.clone())
.or_insert_with(SummaryStats::default);
by_binary.runs += 1;
if !entry.success {
by_binary.failures += 1;
}
by_binary.total_ms = by_binary.total_ms.saturating_add(entry.duration_ms);
let by_label = report
.by_label
.entry(entry.label.clone())
.or_insert_with(SummaryStats::default);
by_label.runs += 1;
if !entry.success {
by_label.failures += 1;
}
by_label.total_ms = by_label.total_ms.saturating_add(entry.duration_ms);
let comparison = report
.comparisons
.entry(entry.label.clone())
.or_insert_with(ComparisonStats::default);
if entry.binary == "br" {
comparison.br_runs += 1;
comparison.br_total_ms = comparison.br_total_ms.saturating_add(entry.duration_ms);
} else if entry.binary == "bd" {
comparison.bd_runs += 1;
comparison.bd_total_ms = comparison.bd_total_ms.saturating_add(entry.duration_ms);
}
}
for comparison in report.comparisons.values_mut() {
if comparison.br_total_ms > 0 && comparison.bd_total_ms > 0 {
comparison.speedup_bd_over_br =
Some(comparison.bd_total_ms as f64 / comparison.br_total_ms as f64);
}
}
let summary_path = log_dir.join("conformance_summary.json");
let json = serde_json::to_string_pretty(&report).expect("serialize summary");
fs::write(summary_path, json).expect("write summary");
}
fn xml_escape(input: &str) -> String {
input
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn write_junit(log_dir: &PathBuf, entries: &[RunLogEntry]) {
let total = entries.len();
let failures = entries.iter().filter(|e| !e.success).count();
let mut xml = String::new();
xml.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
xml.push('\n');
xml.push_str(&format!(
r#"<testsuite name="conformance_runs" tests="{total}" failures="{failures}">"#
));
xml.push('\n');
for entry in entries {
let name = xml_escape(&format!("{}:{}", entry.binary, entry.label));
let classname = xml_escape(&entry.binary);
let time_secs = entry.duration_ms as f64 / 1000.0;
xml.push_str(&format!(
r#" <testcase classname="{classname}" name="{name}" time="{time_secs:.3}">"#
));
if !entry.success {
let msg = xml_escape(&format!(
"exit={}; log={}",
entry.status_code, entry.log_path
));
xml.push_str(&format!(r#"<failure message="{msg}"/>"#));
}
xml.push_str("</testcase>\n");
}
xml.push_str("</testsuite>\n");
let junit_path = log_dir.join("conformance_junit.xml");
fs::write(junit_path, xml).expect("write junit xml");
}
fn write_failure_context(
log_dir: &PathBuf,
entry: &RunLogEntry,
stdout: &str,
stderr: &str,
cwd: &PathBuf,
) {
let beads_dir = cwd.join(".beads");
let context = serde_json::json!({
"timestamp": entry.timestamp,
"label": entry.label,
"binary": entry.binary,
"args": entry.args,
"cwd": entry.cwd,
"status_code": entry.status_code,
"success": entry.success,
"duration_ms": entry.duration_ms,
"stdout_len": entry.stdout_len,
"stderr_len": entry.stderr_len,
"stdout_preview": stdout.chars().take(2000).collect::<String>(),
"stderr_preview": stderr.chars().take(2000).collect::<String>(),
"beads_dir": beads_dir.display().to_string(),
"beads_entries": collect_dir_listing(&beads_dir),
"recent_runs": read_run_entries(log_dir).into_iter().rev().take(5).collect::<Vec<_>>(),
});
let path = log_dir.join(format!("{}.failure.json", entry.label));
let json = serde_json::to_string_pretty(&context).expect("serialize failure context");
fs::write(path, json).expect("write failure context");
}
fn record_run(log_dir: &PathBuf, entry: RunLogEntry, stdout: &str, stderr: &str, cwd: &PathBuf) {
let config = LogConfig::from_env();
if !(config.json_logs || config.junit || config.summary || config.failure_context) {
return;
}
let _guard = log_mutex().lock().expect("lock test log mutex");
append_run_entry(log_dir, &entry);
let entries = read_run_entries(log_dir);
if config.summary {
update_summary(log_dir, &entries);
}
if config.junit {
write_junit(log_dir, &entries);
}
if config.failure_context && !entry.success {
write_failure_context(log_dir, &entry, stdout, stderr, cwd);
}
}
#[derive(Debug, Clone)]
pub struct BenchmarkConfig {
pub warmup_runs: usize,
pub timed_runs: usize,
pub outlier_threshold: f64,
}
impl Default for BenchmarkConfig {
fn default() -> Self {
Self {
warmup_runs: 2,
timed_runs: 5,
outlier_threshold: 2.0,
}
}
}
#[derive(Debug, Clone)]
pub struct TimingStats {
pub mean_ms: f64,
pub median_ms: f64,
pub p95_ms: f64,
pub std_dev_ms: f64,
pub min_ms: f64,
pub max_ms: f64,
pub run_count: usize,
}
impl TimingStats {
pub fn from_durations(durations: &[Duration]) -> Self {
if durations.is_empty() {
return Self {
mean_ms: 0.0,
median_ms: 0.0,
p95_ms: 0.0,
std_dev_ms: 0.0,
min_ms: 0.0,
max_ms: 0.0,
run_count: 0,
};
}
let mut ms_values: Vec<f64> = durations.iter().map(|d| d.as_secs_f64() * 1000.0).collect();
ms_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = ms_values.len();
let mean = ms_values.iter().sum::<f64>() / n as f64;
let median = if n % 2 == 0 {
(ms_values[n / 2 - 1] + ms_values[n / 2]) / 2.0
} else {
ms_values[n / 2]
};
let p95_idx = (n as f64 * 0.95).ceil() as usize - 1;
let p95 = ms_values[p95_idx.min(n - 1)];
let variance = ms_values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n as f64;
let std_dev = variance.sqrt();
Self {
mean_ms: mean,
median_ms: median,
p95_ms: p95,
std_dev_ms: std_dev,
min_ms: ms_values[0],
max_ms: ms_values[n - 1],
run_count: n,
}
}
pub fn filter_outliers(durations: &[Duration], threshold: f64) -> Vec<Duration> {
if durations.len() < 3 {
return durations.to_vec();
}
let ms_values: Vec<f64> = durations.iter().map(|d| d.as_secs_f64() * 1000.0).collect();
let mean = ms_values.iter().sum::<f64>() / ms_values.len() as f64;
let variance =
ms_values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / ms_values.len() as f64;
let std_dev = variance.sqrt();
durations
.iter()
.zip(ms_values.iter())
.filter(|&(_, &ms)| (ms - mean).abs() <= threshold * std_dev)
.map(|(d, _)| *d)
.collect()
}
}
pub fn run_benchmark<F>(config: &BenchmarkConfig, mut f: F) -> TimingStats
where
F: FnMut() -> Duration,
{
for _ in 0..config.warmup_runs {
let _ = f();
}
let mut durations: Vec<Duration> = Vec::with_capacity(config.timed_runs);
for _ in 0..config.timed_runs {
durations.push(f());
}
let filtered = TimingStats::filter_outliers(&durations, config.outlier_threshold);
TimingStats::from_durations(&filtered)
}
pub fn normalize_json(json_str: &str) -> Result<Value, serde_json::Error> {
let mut value: Value = serde_json::from_str(json_str)?;
normalize_value(&mut value);
Ok(value)
}
fn normalize_value(value: &mut Value) {
match value {
Value::Object(map) => {
let timestamp_fields: HashSet<&str> = [
"created_at",
"updated_at",
"closed_at",
"deleted_at",
"due_at",
"defer_until",
"compacted_at",
]
.into_iter()
.collect();
for (key, val) in map.iter_mut() {
if timestamp_fields.contains(key.as_str()) {
if val.is_string() {
*val = Value::String("NORMALIZED_TIMESTAMP".to_string());
}
} else if key == "id" || key == "issue_id" || key == "depends_on_id" {
if let Some(s) = val.as_str() {
if let Some(dash_pos) = s.find('-') {
let prefix = &s[..dash_pos];
*val = Value::String(format!("{prefix}-NORMALIZED"));
}
}
} else if key == "content_hash" {
if val.is_string() {
*val = Value::String("NORMALIZED_HASH".to_string());
}
} else {
normalize_value(val);
}
}
}
Value::Array(arr) => {
for item in arr.iter_mut() {
normalize_value(item);
}
}
_ => {}
}
}
fn normalize_path_fields(value: &mut Value, workspace_root: &Path) {
let root = workspace_root_string(workspace_root);
normalize_path_fields_inner(value, &root);
}
fn workspace_root_string(root: &Path) -> String {
root.canonicalize()
.unwrap_or_else(|_| root.to_path_buf())
.display()
.to_string()
}
fn normalize_path_fields_inner(value: &mut Value, root: &str) {
match value {
Value::Object(map) => {
for (key, val) in map.iter_mut() {
if is_path_key(key) {
if let Some(s) = val.as_str() {
*val = Value::String(normalize_path_value(s, root));
}
} else {
normalize_path_fields_inner(val, root);
}
}
}
Value::Array(arr) => {
for item in arr.iter_mut() {
normalize_path_fields_inner(item, root);
}
}
_ => {}
}
}
fn normalize_path_value(value: &str, root: &str) -> String {
let mut normalized = value.replace('\\', "/");
let root_norm = root.replace('\\', "/");
if normalized.starts_with(&root_norm) {
normalized = format!("<WORKSPACE>{}", &normalized[root_norm.len()..]);
}
normalized
}
fn is_path_key(key: &str) -> bool {
matches!(
key,
"path" | "database_path" | "beads_dir" | "jsonl_path" | "redirected_from" | "socket_path"
)
}
pub fn compare_json(br_output: &str, bd_output: &str, mode: &CompareMode) -> Result<(), String> {
match mode {
CompareMode::ExactJson => {
let br_json: Value =
serde_json::from_str(br_output).map_err(|e| format!("br JSON parse: {e}"))?;
let bd_json: Value =
serde_json::from_str(bd_output).map_err(|e| format!("bd JSON parse: {e}"))?;
if br_json != bd_json {
return Err(format!(
"JSON mismatch\nbr: {}\nbd: {}",
serde_json::to_string_pretty(&br_json).unwrap_or_default(),
serde_json::to_string_pretty(&bd_json).unwrap_or_default()
));
}
}
CompareMode::NormalizedJson => {
let br_json = normalize_json(br_output).map_err(|e| format!("br JSON parse: {e}"))?;
let bd_json = normalize_json(bd_output).map_err(|e| format!("bd JSON parse: {e}"))?;
if br_json != bd_json {
return Err(format!(
"Normalized JSON mismatch\nbr: {}\nbd: {}",
serde_json::to_string_pretty(&br_json).unwrap_or_default(),
serde_json::to_string_pretty(&bd_json).unwrap_or_default()
));
}
}
CompareMode::ContainsFields(fields) => {
let br_json: Value =
serde_json::from_str(br_output).map_err(|e| format!("br JSON parse: {e}"))?;
let bd_json: Value =
serde_json::from_str(bd_output).map_err(|e| format!("bd JSON parse: {e}"))?;
for field in fields {
let br_val = extract_field(&br_json, field);
let bd_val = extract_field(&bd_json, field);
if br_val != bd_val {
return Err(format!(
"Field '{}' mismatch\nbr: {:?}\nbd: {:?}",
field, br_val, bd_val
));
}
}
}
CompareMode::ExitCodeOnly => {
}
CompareMode::ArrayUnordered => {
let br_json: Value =
serde_json::from_str(br_output).map_err(|e| format!("br JSON parse: {e}"))?;
let bd_json: Value =
serde_json::from_str(bd_output).map_err(|e| format!("bd JSON parse: {e}"))?;
if !json_equal_unordered(&br_json, &bd_json) {
return Err(format!(
"Array-unordered mismatch\nbr: {}\nbd: {}",
serde_json::to_string_pretty(&br_json).unwrap_or_default(),
serde_json::to_string_pretty(&bd_json).unwrap_or_default()
));
}
}
CompareMode::FieldsExcluded(excluded) => {
let br_json: Value =
serde_json::from_str(br_output).map_err(|e| format!("br JSON parse: {e}"))?;
let bd_json: Value =
serde_json::from_str(bd_output).map_err(|e| format!("bd JSON parse: {e}"))?;
let br_filtered = filter_fields(&br_json, excluded);
let bd_filtered = filter_fields(&bd_json, excluded);
if br_filtered != bd_filtered {
return Err(format!(
"Fields-excluded mismatch\nbr: {}\nbd: {}",
serde_json::to_string_pretty(&br_filtered).unwrap_or_default(),
serde_json::to_string_pretty(&bd_filtered).unwrap_or_default()
));
}
}
CompareMode::StructureOnly => {
let br_json: Value =
serde_json::from_str(br_output).map_err(|e| format!("br JSON parse: {e}"))?;
let bd_json: Value =
serde_json::from_str(bd_output).map_err(|e| format!("bd JSON parse: {e}"))?;
if !structure_matches(&br_json, &bd_json) {
return Err(format!(
"Structure mismatch\nbr: {}\nbd: {}",
serde_json::to_string_pretty(&br_json).unwrap_or_default(),
serde_json::to_string_pretty(&bd_json).unwrap_or_default()
));
}
}
}
Ok(())
}
fn log_timings(test_name: &str, br: &CmdOutput, bd: &CmdOutput) {
info!("conformance_{}: br_timing={:?}", test_name, br.duration);
info!("conformance_{}: bd_timing={:?}", test_name, bd.duration);
if br.duration.as_nanos() > 0 {
let speedup = bd.duration.as_secs_f64() / br.duration.as_secs_f64();
info!("conformance_{}: speedup={:.2}x", test_name, speedup);
}
}
fn extract_field<'a>(json: &'a Value, field: &str) -> Option<&'a Value> {
let mut current = json;
for part in field.split('.') {
match current {
Value::Object(map) => {
current = map.get(part)?;
}
Value::Array(arr) if !arr.is_empty() => {
if let Value::Object(map) = &arr[0] {
current = map.get(part)?;
} else {
return None;
}
}
_ => return None,
}
}
Some(current)
}
fn json_equal_unordered(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Array(arr_a), Value::Array(arr_b)) => {
if arr_a.len() != arr_b.len() {
return false;
}
for elem_a in arr_a {
if !arr_b
.iter()
.any(|elem_b| json_equal_unordered(elem_a, elem_b))
{
return false;
}
}
true
}
(Value::Object(map_a), Value::Object(map_b)) => {
if map_a.len() != map_b.len() {
return false;
}
for (key, val_a) in map_a {
match map_b.get(key) {
Some(val_b) => {
if !json_equal_unordered(val_a, val_b) {
return false;
}
}
None => return false,
}
}
true
}
_ => a == b,
}
}
fn filter_fields(json: &Value, excluded: &[String]) -> Value {
match json {
Value::Object(map) => {
let filtered: serde_json::Map<String, Value> = map
.iter()
.filter(|(k, _)| !excluded.contains(k))
.map(|(k, v)| (k.clone(), filter_fields(v, excluded)))
.collect();
Value::Object(filtered)
}
Value::Array(arr) => Value::Array(arr.iter().map(|v| filter_fields(v, excluded)).collect()),
other => other.clone(),
}
}
fn structure_matches(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Object(map_a), Value::Object(map_b)) => {
if map_a.len() != map_b.len() {
return false;
}
for (key, val_a) in map_a {
match map_b.get(key) {
Some(val_b) => {
if !structure_matches(val_a, val_b) {
return false;
}
}
None => return false,
}
}
true
}
(Value::Array(arr_a), Value::Array(arr_b)) => {
if arr_a.is_empty() && arr_b.is_empty() {
return true;
}
if arr_a.is_empty() != arr_b.is_empty() {
return false;
}
structure_matches(&arr_a[0], &arr_b[0])
}
(Value::Null, Value::Null)
| (Value::Bool(_), Value::Bool(_))
| (Value::Number(_), Value::Number(_))
| (Value::String(_), Value::String(_)) => true,
_ => false,
}
}
pub fn diff_json(br: &Value, bd: &Value) -> String {
let mut diffs = Vec::new();
collect_diffs(br, bd, "", &mut diffs);
if diffs.is_empty() {
return "No differences found".to_string();
}
let mut output = String::new();
output.push_str("Differences found:\n");
for (path, br_val, bd_val) in diffs.iter().take(20) {
output.push_str(&format!(
" {}: br={}, bd={}\n",
if path.is_empty() { "(root)" } else { path },
br_val,
bd_val
));
}
if diffs.len() > 20 {
output.push_str(&format!(
" ... and {} more differences\n",
diffs.len() - 20
));
}
output
}
fn collect_diffs(br: &Value, bd: &Value, path: &str, diffs: &mut Vec<(String, String, String)>) {
match (br, bd) {
(Value::Object(br_map), Value::Object(bd_map)) => {
for key in br_map.keys() {
if !bd_map.contains_key(key) {
let key_path = format_path(path, key);
diffs.push((
key_path,
format_value_short(&br_map[key]),
"(missing)".to_string(),
));
}
}
for key in bd_map.keys() {
if !br_map.contains_key(key) {
let key_path = format_path(path, key);
diffs.push((
key_path,
"(missing)".to_string(),
format_value_short(&bd_map[key]),
));
}
}
for (key, br_val) in br_map {
if let Some(bd_val) = bd_map.get(key) {
collect_diffs(br_val, bd_val, &format_path(path, key), diffs);
}
}
}
(Value::Array(br_arr), Value::Array(bd_arr)) => {
if br_arr.len() != bd_arr.len() {
diffs.push((
format!("{}.length", path),
br_arr.len().to_string(),
bd_arr.len().to_string(),
));
}
let min_len = br_arr.len().min(bd_arr.len());
for i in 0..min_len {
collect_diffs(&br_arr[i], &bd_arr[i], &format!("{}[{}]", path, i), diffs);
}
}
_ => {
if br != bd {
diffs.push((
path.to_string(),
format_value_short(br),
format_value_short(bd),
));
}
}
}
}
fn format_path(base: &str, key: &str) -> String {
if base.is_empty() {
key.to_string()
} else {
format!("{}.{}", base, key)
}
}
fn format_value_short(val: &Value) -> String {
match val {
Value::Null => "null".to_string(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
Value::String(s) => {
if s.len() > 30 {
format!("\"{}...\"", &s[..27])
} else {
format!("\"{}\"", s)
}
}
Value::Array(arr) => format!("[{} items]", arr.len()),
Value::Object(map) => format!("{{...{} keys}}", map.len()),
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct TestScenario {
pub name: String,
pub description: String,
pub setup_commands: Vec<Vec<String>>,
pub test_command: Vec<String>,
pub compare_mode: CompareMode,
pub compare_exit_codes: bool,
}
impl TestScenario {
#[allow(dead_code)]
pub fn new(name: &str, test_command: Vec<&str>) -> Self {
Self {
name: name.to_string(),
description: String::new(),
setup_commands: Vec::new(),
test_command: test_command.into_iter().map(String::from).collect(),
compare_mode: CompareMode::NormalizedJson,
compare_exit_codes: true,
}
}
#[allow(dead_code)]
pub fn with_description(mut self, desc: &str) -> Self {
self.description = desc.to_string();
self
}
#[allow(dead_code)]
pub fn with_setup(mut self, commands: Vec<Vec<&str>>) -> Self {
self.setup_commands = commands
.into_iter()
.map(|cmd| cmd.into_iter().map(String::from).collect())
.collect();
self
}
#[allow(dead_code)]
pub fn with_compare_mode(mut self, mode: CompareMode) -> Self {
self.compare_mode = mode;
self
}
#[allow(dead_code)]
pub fn execute(&self, workspace: &ConformanceWorkspace) -> Result<(), String> {
for cmd in &self.setup_commands {
let args: Vec<&str> = cmd.iter().map(String::as_str).collect();
let br_result = workspace.run_br(args.clone(), &format!("setup_{}", self.name));
let bd_result = workspace.run_bd(args, &format!("setup_{}", self.name));
if !br_result.status.success() {
return Err(format!("br setup failed: {}", br_result.stderr));
}
if !bd_result.status.success() {
return Err(format!("bd setup failed: {}", bd_result.stderr));
}
}
let args: Vec<&str> = self.test_command.iter().map(String::as_str).collect();
let br_result = workspace.run_br(args.clone(), &self.name);
let bd_result = workspace.run_bd(args, &self.name);
if self.compare_exit_codes {
let br_success = br_result.status.success();
let bd_success = bd_result.status.success();
if br_success != bd_success {
return Err(format!(
"Exit code mismatch: br={}, bd={}",
br_result.status, bd_result.status
));
}
}
let br_json = extract_json_payload(&br_result.stdout);
let bd_json = extract_json_payload(&bd_result.stdout);
compare_json(&br_json, &bd_json, &self.compare_mode)
}
}
#[allow(dead_code)]
pub mod scenarios {
use super::*;
pub fn empty_list() -> TestScenario {
TestScenario::new("empty_list", vec!["list", "--json"])
.with_description("Verify empty list output matches")
}
pub fn create_basic() -> TestScenario {
TestScenario::new("create_basic", vec!["list", "--json"])
.with_description("Create a basic issue and verify list output")
.with_setup(vec![vec!["create", "Test issue"]])
.with_compare_mode(CompareMode::ContainsFields(vec![
"title".to_string(),
"status".to_string(),
"issue_type".to_string(),
]))
}
pub fn create_with_type_and_priority() -> TestScenario {
TestScenario::new("create_typed", vec!["list", "--json"])
.with_description("Create issue with type and priority")
.with_setup(vec![vec![
"create",
"Bug issue",
"--type",
"bug",
"--priority",
"1",
]])
.with_compare_mode(CompareMode::ContainsFields(vec![
"title".to_string(),
"issue_type".to_string(),
"priority".to_string(),
]))
}
pub fn stats_after_create() -> TestScenario {
TestScenario::new("stats_after_create", vec!["stats", "--json"])
.with_description("Verify stats after creating issues")
.with_setup(vec![vec!["create", "Issue 1"], vec!["create", "Issue 2"]])
.with_compare_mode(CompareMode::ContainsFields(vec!["total".to_string()]))
}
}
#[test]
fn conformance_init() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_init test");
let workspace = ConformanceWorkspace::new();
let (br_out, bd_out) = workspace.init_both();
assert!(br_out.status.success(), "br init failed: {}", br_out.stderr);
assert!(bd_out.status.success(), "bd init failed: {}", bd_out.stderr);
assert!(
workspace.br_root.join(".beads").exists(),
"br did not create .beads"
);
assert!(
workspace.bd_root.join(".beads").exists(),
"bd did not create .beads"
);
info!("conformance_init passed");
}
#[test]
fn conformance_create_basic() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_create_basic test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Test issue", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Test issue", "--json"], "create");
assert!(
br_create.status.success(),
"br create failed: {}",
br_create.stderr
);
assert!(
bd_create.status.success(),
"bd create failed: {}",
bd_create.stderr
);
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let result = compare_json(
&br_json,
&bd_json,
&CompareMode::ContainsFields(vec![
"title".to_string(),
"status".to_string(),
"issue_type".to_string(),
]),
);
assert!(result.is_ok(), "JSON comparison failed: {:?}", result.err());
info!("conformance_create_basic passed");
}
#[test]
fn conformance_create_with_type_and_priority() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_create_with_type_and_priority test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let args = [
"create",
"Bug fix needed",
"--type",
"bug",
"--priority",
"1",
"--json",
];
let br_create = workspace.run_br(args.clone(), "create_bug");
let bd_create = workspace.run_bd(args, "create_bug");
assert!(
br_create.status.success(),
"br create failed: {}",
br_create.stderr
);
assert!(
bd_create.status.success(),
"bd create failed: {}",
bd_create.stderr
);
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("br json");
let bd_val: Value = serde_json::from_str(&bd_json).expect("bd json");
let br_issue = if br_val.is_array() {
&br_val[0]
} else {
&br_val
};
let bd_issue = if bd_val.is_array() {
&bd_val[0]
} else {
&bd_val
};
assert_eq!(br_issue["title"], bd_issue["title"], "title mismatch");
assert_eq!(
br_issue["issue_type"], bd_issue["issue_type"],
"issue_type mismatch: br={}, bd={}",
br_issue["issue_type"], bd_issue["issue_type"]
);
assert_eq!(
br_issue["priority"], bd_issue["priority"],
"priority mismatch: br={}, bd={}",
br_issue["priority"], bd_issue["priority"]
);
info!("conformance_create_with_type_and_priority passed");
}
#[test]
fn conformance_list_empty() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_list_empty test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_list = workspace.run_br(["list", "--json"], "list_empty");
let bd_list = workspace.run_bd(["list", "--json"], "list_empty");
assert!(
br_list.status.success(),
"br list failed: {}",
br_list.stderr
);
assert!(
bd_list.status.success(),
"bd list failed: {}",
bd_list.stderr
);
let br_json = extract_json_payload(&br_list.stdout);
let bd_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_json).unwrap_or(Value::Null);
let bd_val: Value = serde_json::from_str(&bd_json).unwrap_or(Value::Null);
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"list lengths differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 0, "expected empty list");
info!("conformance_list_empty passed");
}
#[test]
#[ignore = "bd v0.46.0 list --json returns empty: known behavioral difference"]
fn conformance_list_with_issues() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_list_with_issues test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Issue one"], "create1");
workspace.run_br_in_bd_env(["create", "Issue one"], "create1");
workspace.run_br(["create", "Issue two"], "create2");
workspace.run_br_in_bd_env(["create", "Issue two"], "create2");
let br_list = workspace.run_br(["list", "--json"], "list");
let bd_list = workspace.run_bd(["list", "--json"], "list");
assert!(
br_list.status.success(),
"br list failed: {}",
br_list.stderr
);
assert!(
bd_list.status.success(),
"bd list failed: {}",
bd_list.stderr
);
let br_json = extract_json_payload(&br_list.stdout);
let bd_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("br json");
let bd_val: Value = serde_json::from_str(&bd_json).expect("bd json");
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"list lengths differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 2, "expected 2 issues");
info!("conformance_list_with_issues passed");
}
#[test]
fn conformance_ready_empty() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_ready_empty test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_ready = workspace.run_br(["ready", "--json"], "ready_empty");
let bd_ready = workspace.run_bd(["ready", "--json"], "ready_empty");
assert!(
br_ready.status.success(),
"br ready failed: {}",
br_ready.stderr
);
assert!(
bd_ready.status.success(),
"bd ready failed: {}",
bd_ready.stderr
);
let br_json = extract_json_payload(&br_ready.stdout);
let bd_json = extract_json_payload(&bd_ready.stdout);
let br_val: Value = serde_json::from_str(&br_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_json).unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"ready lengths differ: br={}, bd={}",
br_len, bd_len
);
info!("conformance_ready_empty passed");
}
#[test]
fn conformance_ready_with_issues() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_ready_with_issues test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Ready issue"], "create");
workspace.run_bd(["create", "Ready issue"], "create");
let br_ready = workspace.run_br(["ready", "--json"], "ready");
let bd_ready = workspace.run_bd(["ready", "--json"], "ready");
assert!(
br_ready.status.success(),
"br ready failed: {}",
br_ready.stderr
);
assert!(
bd_ready.status.success(),
"bd ready failed: {}",
bd_ready.stderr
);
let br_json = extract_json_payload(&br_ready.stdout);
let bd_json = extract_json_payload(&bd_ready.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("br json");
let bd_val: Value = serde_json::from_str(&bd_json).expect("bd json");
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"ready lengths differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 1, "expected 1 ready issue");
info!("conformance_ready_with_issues passed");
}
#[test]
fn conformance_ready_with_deps() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_ready_with_deps test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_blocker = workspace.run_br(["create", "Blocker issue", "--json"], "create_blocker");
let bd_blocker = workspace.run_bd(["create", "Blocker issue", "--json"], "create_blocker");
let br_blocked = workspace.run_br(["create", "Blocked issue", "--json"], "create_blocked");
let bd_blocked = workspace.run_bd(["create", "Blocked issue", "--json"], "create_blocked");
assert!(br_blocker.status.success());
assert!(bd_blocker.status.success());
assert!(br_blocked.status.success());
assert!(bd_blocked.status.success());
let br_blocker_json: Value =
serde_json::from_str(&extract_json_payload(&br_blocker.stdout)).expect("br json");
let bd_blocker_json: Value =
serde_json::from_str(&extract_json_payload(&bd_blocker.stdout)).expect("bd json");
let br_blocked_json: Value =
serde_json::from_str(&extract_json_payload(&br_blocked.stdout)).expect("br json");
let bd_blocked_json: Value =
serde_json::from_str(&extract_json_payload(&bd_blocked.stdout)).expect("bd json");
let br_blocker_id = br_blocker_json["id"].as_str().expect("br blocker id");
let bd_blocker_id = bd_blocker_json["id"].as_str().expect("bd blocker id");
let br_blocked_id = br_blocked_json["id"].as_str().expect("br blocked id");
let bd_blocked_id = bd_blocked_json["id"].as_str().expect("bd blocked id");
let br_dep = workspace.run_br(["dep", "add", br_blocked_id, br_blocker_id], "dep_add");
let bd_dep = workspace.run_bd(["dep", "add", bd_blocked_id, bd_blocker_id], "dep_add");
assert!(
br_dep.status.success(),
"br dep add failed: {}",
br_dep.stderr
);
assert!(
bd_dep.status.success(),
"bd dep add failed: {}",
bd_dep.stderr
);
let br_ready = workspace.run_br(["ready", "--json"], "ready_with_deps");
let bd_ready = workspace.run_bd(["ready", "--json"], "ready_with_deps");
assert!(
br_ready.status.success(),
"br ready failed: {}",
br_ready.stderr
);
assert!(
bd_ready.status.success(),
"bd ready failed: {}",
bd_ready.stderr
);
let br_val: Value = serde_json::from_str(&extract_json_payload(&br_ready.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&extract_json_payload(&bd_ready.stdout))
.unwrap_or(Value::Array(vec![]));
let br_ids: Vec<&str> = br_val
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.get("id").and_then(|id| id.as_str()))
.collect()
})
.unwrap_or_default();
let bd_ids: Vec<&str> = bd_val
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.get("id").and_then(|id| id.as_str()))
.collect()
})
.unwrap_or_default();
assert_eq!(br_ids.len(), bd_ids.len(), "ready lengths differ");
assert!(
br_ids.contains(&br_blocker_id),
"br ready should include blocker"
);
assert!(
!br_ids.contains(&br_blocked_id),
"br ready should exclude blocked issue"
);
assert!(
bd_ids.contains(&bd_blocker_id),
"bd ready should include blocker"
);
assert!(
!bd_ids.contains(&bd_blocked_id),
"bd ready should exclude blocked issue"
);
info!("conformance_ready_with_deps passed");
}
#[test]
fn conformance_ready_limit() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_ready_limit test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
for i in 0..3 {
let title = format!("Ready issue {}", i);
workspace.run_br(["create", &title], &format!("ready_limit_br_{i}"));
workspace.run_bd(["create", &title], &format!("ready_limit_bd_{i}"));
}
let br_ready = workspace.run_br(["ready", "--json", "--limit", "1"], "ready_limit");
let bd_ready = workspace.run_bd(["ready", "--json", "--limit", "1"], "ready_limit");
assert!(
br_ready.status.success(),
"br ready failed: {}",
br_ready.stderr
);
assert!(
bd_ready.status.success(),
"bd ready failed: {}",
bd_ready.stderr
);
let br_val: Value = serde_json::from_str(&extract_json_payload(&br_ready.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&extract_json_payload(&bd_ready.stdout))
.unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(br_len, 1, "br ready should honor limit");
assert_eq!(bd_len, 1, "bd ready should honor limit");
info!("conformance_ready_limit passed");
}
#[test]
fn conformance_ready_filter_type() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_ready_filter_type test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_bug = workspace.run_br(
["create", "Bug issue", "--type", "bug", "--json"],
"ready_bug",
);
let bd_bug = workspace.run_bd(
["create", "Bug issue", "--type", "bug", "--json"],
"ready_bug",
);
let _br_task = workspace.run_br(["create", "Task issue", "--json"], "ready_task");
let _bd_task = workspace.run_bd(["create", "Task issue", "--json"], "ready_task");
let br_bug_id = serde_json::from_str::<Value>(&extract_json_payload(&br_bug.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("br bug id");
let bd_bug_id = serde_json::from_str::<Value>(&extract_json_payload(&bd_bug.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("bd bug id");
let br_ready = workspace.run_br(["ready", "--json", "--type", "bug"], "ready_type");
let bd_ready = workspace.run_bd(["ready", "--json", "--type", "bug"], "ready_type");
assert!(
br_ready.status.success(),
"br ready failed: {}",
br_ready.stderr
);
assert!(
bd_ready.status.success(),
"bd ready failed: {}",
bd_ready.stderr
);
let br_val: Value = serde_json::from_str(&extract_json_payload(&br_ready.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&extract_json_payload(&bd_ready.stdout))
.unwrap_or(Value::Array(vec![]));
let br_ids: Vec<&str> = br_val
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.get("id").and_then(|id| id.as_str()))
.collect()
})
.unwrap_or_default();
let bd_ids: Vec<&str> = bd_val
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.get("id").and_then(|id| id.as_str()))
.collect()
})
.unwrap_or_default();
assert_eq!(br_ids.len(), 1, "br ready should filter to 1 bug");
assert_eq!(bd_ids.len(), 1, "bd ready should filter to 1 bug");
assert_eq!(br_ids[0], br_bug_id);
assert_eq!(bd_ids[0], bd_bug_id);
info!("conformance_ready_filter_type passed");
}
#[test]
fn conformance_ready_filter_assignee() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_ready_filter_assignee test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_assigned = workspace.run_br(
["create", "Assigned issue", "--assignee", "alice", "--json"],
"assignee",
);
let bd_assigned = workspace.run_bd(
["create", "Assigned issue", "--assignee", "alice", "--json"],
"assignee",
);
let _br_unassigned = workspace.run_br(["create", "Unassigned issue"], "unassigned");
let _bd_unassigned = workspace.run_bd(["create", "Unassigned issue"], "unassigned");
let br_assigned_id = serde_json::from_str::<Value>(&extract_json_payload(&br_assigned.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("br assigned id");
let bd_assigned_id = serde_json::from_str::<Value>(&extract_json_payload(&bd_assigned.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("bd assigned id");
let br_ready = workspace.run_br(["ready", "--json", "--assignee", "alice"], "ready_assignee");
let bd_ready = workspace.run_bd(["ready", "--json", "--assignee", "alice"], "ready_assignee");
assert!(
br_ready.status.success(),
"br ready failed: {}",
br_ready.stderr
);
assert!(
bd_ready.status.success(),
"bd ready failed: {}",
bd_ready.stderr
);
let br_val: Value = serde_json::from_str(&extract_json_payload(&br_ready.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&extract_json_payload(&bd_ready.stdout))
.unwrap_or(Value::Array(vec![]));
let br_ids: Vec<&str> = br_val
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.get("id").and_then(|id| id.as_str()))
.collect()
})
.unwrap_or_default();
let bd_ids: Vec<&str> = bd_val
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.get("id").and_then(|id| id.as_str()))
.collect()
})
.unwrap_or_default();
assert_eq!(br_ids.len(), 1, "br ready should filter to 1 assignee");
assert_eq!(bd_ids.len(), 1, "bd ready should filter to 1 assignee");
assert_eq!(br_ids[0], br_assigned_id);
assert_eq!(bd_ids[0], bd_assigned_id);
info!("conformance_ready_filter_assignee passed");
}
#[test]
fn conformance_ready_priority_order() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_ready_priority_order test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let priorities = [2, 0, 1];
for (idx, priority) in priorities.iter().enumerate() {
let title = format!("Priority issue {}", idx);
let priority_str = priority.to_string();
let br_out = workspace.run_br(
["create", &title, "-p", &priority_str, "--json"],
&format!("ready_priority_br_{idx}"),
);
let bd_out = workspace.run_bd(
["create", &title, "-p", &priority_str, "--json"],
&format!("ready_priority_bd_{idx}"),
);
assert!(
br_out.status.success(),
"br create failed: {}",
br_out.stderr
);
assert!(
bd_out.status.success(),
"bd create failed: {}",
bd_out.stderr
);
}
let br_ready = workspace.run_br(
["ready", "--json", "--sort", "priority", "--limit", "0"],
"ready_priority",
);
let bd_ready = workspace.run_bd(
["ready", "--json", "--sort", "priority", "--limit", "0"],
"ready_priority",
);
assert!(
br_ready.status.success(),
"br ready failed: {}",
br_ready.stderr
);
assert!(
bd_ready.status.success(),
"bd ready failed: {}",
bd_ready.stderr
);
let br_val: Value = serde_json::from_str(&extract_json_payload(&br_ready.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&extract_json_payload(&bd_ready.stdout))
.unwrap_or(Value::Array(vec![]));
let br_priorities: Vec<i32> = br_val
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.get("priority").and_then(|p| p.as_i64()))
.map(|p| p as i32)
.collect()
})
.unwrap_or_default();
let bd_priorities: Vec<i32> = bd_val
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.get("priority").and_then(|p| p.as_i64()))
.map(|p| p as i32)
.collect()
})
.unwrap_or_default();
assert_eq!(br_priorities.len(), 3, "br ready should return 3 issues");
assert_eq!(bd_priorities.len(), 3, "bd ready should return 3 issues");
let br_sorted = br_priorities.windows(2).all(|w| w[0] <= w[1]);
let bd_sorted = bd_priorities.windows(2).all(|w| w[0] <= w[1]);
assert!(br_sorted, "br priorities not sorted: {:?}", br_priorities);
assert!(bd_sorted, "bd priorities not sorted: {:?}", bd_priorities);
assert_eq!(
br_priorities,
vec![0, 1, 2],
"br ready priority order mismatch"
);
assert_eq!(
bd_priorities,
vec![0, 1, 2],
"bd ready priority order mismatch"
);
info!("conformance_ready_priority_order passed");
}
#[test]
fn conformance_ready_json_shape() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_ready_json_shape test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Ready json shape"], "ready_json_shape_br");
workspace.run_bd(["create", "Ready json shape"], "ready_json_shape_bd");
let br_ready = workspace.run_br(["ready", "--json"], "ready_json_shape");
let bd_ready = workspace.run_bd(["ready", "--json"], "ready_json_shape");
assert!(
br_ready.status.success(),
"br ready failed: {}",
br_ready.stderr
);
assert!(
bd_ready.status.success(),
"bd ready failed: {}",
bd_ready.stderr
);
let br_json = extract_json_payload(&br_ready.stdout);
let bd_json = extract_json_payload(&bd_ready.stdout);
compare_json(&br_json, &bd_json, &CompareMode::StructureOnly).expect("JSON mismatch");
info!("conformance_ready_json_shape passed");
}
#[test]
fn conformance_blocked_empty() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_blocked_empty test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_blocked = workspace.run_br(["blocked", "--json"], "blocked_empty");
let bd_blocked = workspace.run_bd(["blocked", "--json"], "blocked_empty");
assert!(
br_blocked.status.success(),
"br blocked failed: {}",
br_blocked.stderr
);
assert!(
bd_blocked.status.success(),
"bd blocked failed: {}",
bd_blocked.stderr
);
let br_json = extract_json_payload(&br_blocked.stdout);
let bd_json = extract_json_payload(&bd_blocked.stdout);
let br_val: Value = serde_json::from_str(&br_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_json).unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(br_len, bd_len, "blocked lengths differ");
assert_eq!(br_len, 0, "expected no blocked issues");
info!("conformance_blocked_empty passed");
}
#[test]
fn conformance_blocked_with_deps() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_blocked_with_deps test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_blocker = workspace.run_br(["create", "Blocker issue", "--json"], "create_blocker");
let bd_blocker = workspace.run_bd(["create", "Blocker issue", "--json"], "create_blocker");
let br_blocked = workspace.run_br(["create", "Blocked issue", "--json"], "create_blocked");
let bd_blocked = workspace.run_bd(["create", "Blocked issue", "--json"], "create_blocked");
assert!(br_blocker.status.success());
assert!(bd_blocker.status.success());
assert!(br_blocked.status.success());
assert!(bd_blocked.status.success());
let br_blocker_json: Value =
serde_json::from_str(&extract_json_payload(&br_blocker.stdout)).expect("br json");
let bd_blocker_json: Value =
serde_json::from_str(&extract_json_payload(&bd_blocker.stdout)).expect("bd json");
let br_blocked_json: Value =
serde_json::from_str(&extract_json_payload(&br_blocked.stdout)).expect("br json");
let bd_blocked_json: Value =
serde_json::from_str(&extract_json_payload(&bd_blocked.stdout)).expect("bd json");
let br_blocker_id = br_blocker_json["id"].as_str().expect("br blocker id");
let bd_blocker_id = bd_blocker_json["id"].as_str().expect("bd blocker id");
let br_blocked_id = br_blocked_json["id"].as_str().expect("br blocked id");
let bd_blocked_id = bd_blocked_json["id"].as_str().expect("bd blocked id");
let br_dep = workspace.run_br(["dep", "add", br_blocked_id, br_blocker_id], "dep_add");
let bd_dep = workspace.run_bd(["dep", "add", bd_blocked_id, bd_blocker_id], "dep_add");
assert!(
br_dep.status.success(),
"br dep add failed: {}",
br_dep.stderr
);
assert!(
bd_dep.status.success(),
"bd dep add failed: {}",
bd_dep.stderr
);
let br_blocked_out = workspace.run_br(["blocked", "--json"], "blocked_with_deps");
let bd_blocked_out = workspace.run_bd(["blocked", "--json"], "blocked_with_deps");
assert!(
br_blocked_out.status.success(),
"br blocked failed: {}",
br_blocked_out.stderr
);
assert!(
bd_blocked_out.status.success(),
"bd blocked failed: {}",
bd_blocked_out.stderr
);
let br_val: Value = serde_json::from_str(&extract_json_payload(&br_blocked_out.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&extract_json_payload(&bd_blocked_out.stdout))
.unwrap_or(Value::Array(vec![]));
let br_ids: Vec<&str> = br_val
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.get("id").and_then(|id| id.as_str()))
.collect()
})
.unwrap_or_default();
let bd_ids: Vec<&str> = bd_val
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.get("id").and_then(|id| id.as_str()))
.collect()
})
.unwrap_or_default();
assert_eq!(br_ids.len(), bd_ids.len(), "blocked lengths differ");
assert!(
br_ids.contains(&br_blocked_id),
"br blocked should include blocked issue"
);
assert!(
bd_ids.contains(&bd_blocked_id),
"bd blocked should include blocked issue"
);
info!("conformance_blocked_with_deps passed");
}
#[test]
fn conformance_blocked_shows_blockers() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_blocked_shows_blockers test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_blocker = workspace.run_br(["create", "Blocker issue", "--json"], "create_blocker");
let bd_blocker = workspace.run_bd(["create", "Blocker issue", "--json"], "create_blocker");
let br_blocked = workspace.run_br(["create", "Blocked issue", "--json"], "create_blocked");
let bd_blocked = workspace.run_bd(["create", "Blocked issue", "--json"], "create_blocked");
let br_blocker_id = serde_json::from_str::<Value>(&extract_json_payload(&br_blocker.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("br blocker id");
let bd_blocker_id = serde_json::from_str::<Value>(&extract_json_payload(&bd_blocker.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("bd blocker id");
let br_blocked_id = serde_json::from_str::<Value>(&extract_json_payload(&br_blocked.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("br blocked id");
let bd_blocked_id = serde_json::from_str::<Value>(&extract_json_payload(&bd_blocked.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("bd blocked id");
let br_dep = workspace.run_br(["dep", "add", &br_blocked_id, &br_blocker_id], "dep_add");
let bd_dep = workspace.run_bd(["dep", "add", &bd_blocked_id, &bd_blocker_id], "dep_add");
assert!(
br_dep.status.success(),
"br dep add failed: {}",
br_dep.stderr
);
assert!(
bd_dep.status.success(),
"bd dep add failed: {}",
bd_dep.stderr
);
let br_blocked_out = workspace.run_br(["blocked", "--json"], "blocked_show_blockers");
let bd_blocked_out = workspace.run_bd(["blocked", "--json"], "blocked_show_blockers");
assert!(
br_blocked_out.status.success(),
"br blocked failed: {}",
br_blocked_out.stderr
);
assert!(
bd_blocked_out.status.success(),
"bd blocked failed: {}",
bd_blocked_out.stderr
);
let br_val: Value =
serde_json::from_str(&extract_json_payload(&br_blocked_out.stdout)).unwrap_or_default();
let bd_val: Value =
serde_json::from_str(&extract_json_payload(&bd_blocked_out.stdout)).unwrap_or_default();
fn has_blocker(val: &Value, blocked_id: &str, blocker_id: &str) -> bool {
let Some(arr) = val.as_array() else {
return false;
};
for item in arr {
if item.get("id").and_then(|v| v.as_str()) != Some(blocked_id) {
continue;
}
if let Some(blocked_by) = item.get("blocked_by").and_then(|v| v.as_array()) {
return blocked_by.iter().any(|entry| {
entry
.as_str()
.map(|s| s.split(':').next().unwrap_or(s) == blocker_id)
.unwrap_or(false)
});
}
}
false
}
assert!(
has_blocker(&br_val, &br_blocked_id, &br_blocker_id),
"br blocked should list blocker"
);
assert!(
has_blocker(&bd_val, &bd_blocked_id, &bd_blocker_id),
"bd blocked should list blocker"
);
info!("conformance_blocked_shows_blockers passed");
}
#[test]
fn conformance_blocked_multiple_blockers() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_blocked_multiple_blockers test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_blocker1 = workspace.run_br(["create", "Blocker 1", "--json"], "blocker1");
let bd_blocker1 = workspace.run_bd(["create", "Blocker 1", "--json"], "blocker1");
let br_blocker2 = workspace.run_br(["create", "Blocker 2", "--json"], "blocker2");
let bd_blocker2 = workspace.run_bd(["create", "Blocker 2", "--json"], "blocker2");
let br_blocked = workspace.run_br(["create", "Blocked issue", "--json"], "blocked_multi");
let bd_blocked = workspace.run_bd(["create", "Blocked issue", "--json"], "blocked_multi");
let br_blocker1_id = serde_json::from_str::<Value>(&extract_json_payload(&br_blocker1.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("br blocker1 id");
let bd_blocker1_id = serde_json::from_str::<Value>(&extract_json_payload(&bd_blocker1.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("bd blocker1 id");
let br_blocker2_id = serde_json::from_str::<Value>(&extract_json_payload(&br_blocker2.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("br blocker2 id");
let bd_blocker2_id = serde_json::from_str::<Value>(&extract_json_payload(&bd_blocker2.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("bd blocker2 id");
let br_blocked_id = serde_json::from_str::<Value>(&extract_json_payload(&br_blocked.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("br blocked id");
let bd_blocked_id = serde_json::from_str::<Value>(&extract_json_payload(&bd_blocked.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("bd blocked id");
let br_dep1 = workspace.run_br(["dep", "add", &br_blocked_id, &br_blocker1_id], "dep_add1");
let br_dep2 = workspace.run_br(["dep", "add", &br_blocked_id, &br_blocker2_id], "dep_add2");
let bd_dep1 = workspace.run_bd(["dep", "add", &bd_blocked_id, &bd_blocker1_id], "dep_add1");
let bd_dep2 = workspace.run_bd(["dep", "add", &bd_blocked_id, &bd_blocker2_id], "dep_add2");
assert!(
br_dep1.status.success(),
"br dep1 failed: {}",
br_dep1.stderr
);
assert!(
br_dep2.status.success(),
"br dep2 failed: {}",
br_dep2.stderr
);
assert!(
bd_dep1.status.success(),
"bd dep1 failed: {}",
bd_dep1.stderr
);
assert!(
bd_dep2.status.success(),
"bd dep2 failed: {}",
bd_dep2.stderr
);
let br_blocked_out = workspace.run_br(["blocked", "--json"], "blocked_multi");
let bd_blocked_out = workspace.run_bd(["blocked", "--json"], "blocked_multi");
assert!(
br_blocked_out.status.success(),
"br blocked failed: {}",
br_blocked_out.stderr
);
assert!(
bd_blocked_out.status.success(),
"bd blocked failed: {}",
bd_blocked_out.stderr
);
let br_val: Value =
serde_json::from_str(&extract_json_payload(&br_blocked_out.stdout)).unwrap_or_default();
let bd_val: Value =
serde_json::from_str(&extract_json_payload(&bd_blocked_out.stdout)).unwrap_or_default();
fn has_blocker(val: &Value, blocked_id: &str, blocker_id: &str) -> bool {
let Some(arr) = val.as_array() else {
return false;
};
for item in arr {
if item.get("id").and_then(|v| v.as_str()) != Some(blocked_id) {
continue;
}
if let Some(blocked_by) = item.get("blocked_by").and_then(|v| v.as_array()) {
return blocked_by.iter().any(|entry| {
entry
.as_str()
.map(|s| s.split(':').next().unwrap_or(s) == blocker_id)
.unwrap_or(false)
});
}
}
false
}
assert!(
has_blocker(&br_val, &br_blocked_id, &br_blocker1_id),
"br blocked should include blocker1"
);
assert!(
has_blocker(&br_val, &br_blocked_id, &br_blocker2_id),
"br blocked should include blocker2"
);
assert!(
has_blocker(&bd_val, &bd_blocked_id, &bd_blocker1_id),
"bd blocked should include blocker1"
);
assert!(
has_blocker(&bd_val, &bd_blocked_id, &bd_blocker2_id),
"bd blocked should include blocker2"
);
info!("conformance_blocked_multiple_blockers passed");
}
#[test]
fn conformance_blocked_chain() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_blocked_chain test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "Blocked A", "--json"], "blocked_a");
let bd_a = workspace.run_bd(["create", "Blocked A", "--json"], "blocked_a");
let br_b = workspace.run_br(["create", "Blocked B", "--json"], "blocked_b");
let bd_b = workspace.run_bd(["create", "Blocked B", "--json"], "blocked_b");
let br_c = workspace.run_br(["create", "Blocker C", "--json"], "blocked_c");
let bd_c = workspace.run_bd(["create", "Blocker C", "--json"], "blocked_c");
let br_a_id = serde_json::from_str::<Value>(&extract_json_payload(&br_a.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("br a id");
let bd_a_id = serde_json::from_str::<Value>(&extract_json_payload(&bd_a.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("bd a id");
let br_b_id = serde_json::from_str::<Value>(&extract_json_payload(&br_b.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("br b id");
let bd_b_id = serde_json::from_str::<Value>(&extract_json_payload(&bd_b.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("bd b id");
let br_c_id = serde_json::from_str::<Value>(&extract_json_payload(&br_c.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("br c id");
let bd_c_id = serde_json::from_str::<Value>(&extract_json_payload(&bd_c.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("bd c id");
let br_dep1 = workspace.run_br(["dep", "add", &br_a_id, &br_b_id], "dep_a_b");
let br_dep2 = workspace.run_br(["dep", "add", &br_b_id, &br_c_id], "dep_b_c");
let bd_dep1 = workspace.run_bd(["dep", "add", &bd_a_id, &bd_b_id], "dep_a_b");
let bd_dep2 = workspace.run_bd(["dep", "add", &bd_b_id, &bd_c_id], "dep_b_c");
assert!(
br_dep1.status.success(),
"br dep a->b failed: {}",
br_dep1.stderr
);
assert!(
br_dep2.status.success(),
"br dep b->c failed: {}",
br_dep2.stderr
);
assert!(
bd_dep1.status.success(),
"bd dep a->b failed: {}",
bd_dep1.stderr
);
assert!(
bd_dep2.status.success(),
"bd dep b->c failed: {}",
bd_dep2.stderr
);
let br_blocked_out = workspace.run_br(["blocked", "--json"], "blocked_chain");
let bd_blocked_out = workspace.run_bd(["blocked", "--json"], "blocked_chain");
assert!(
br_blocked_out.status.success(),
"br blocked failed: {}",
br_blocked_out.stderr
);
assert!(
bd_blocked_out.status.success(),
"bd blocked failed: {}",
bd_blocked_out.stderr
);
let br_val: Value =
serde_json::from_str(&extract_json_payload(&br_blocked_out.stdout)).unwrap_or_default();
let bd_val: Value =
serde_json::from_str(&extract_json_payload(&bd_blocked_out.stdout)).unwrap_or_default();
let br_ids: Vec<&str> = br_val
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.get("id").and_then(|id| id.as_str()))
.collect()
})
.unwrap_or_default();
let bd_ids: Vec<&str> = bd_val
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.get("id").and_then(|id| id.as_str()))
.collect()
})
.unwrap_or_default();
assert!(br_ids.contains(&br_a_id.as_str()));
assert!(br_ids.contains(&br_b_id.as_str()));
assert!(!br_ids.contains(&br_c_id.as_str()));
assert!(bd_ids.contains(&bd_a_id.as_str()));
assert!(bd_ids.contains(&bd_b_id.as_str()));
assert!(!bd_ids.contains(&bd_c_id.as_str()));
info!("conformance_blocked_chain passed");
}
#[test]
fn conformance_blocked_json_shape() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_blocked_json_shape test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_blocker = workspace.run_br(
["create", "Blocker issue", "--json"],
"blocked_shape_blocker",
);
let bd_blocker = workspace.run_bd(
["create", "Blocker issue", "--json"],
"blocked_shape_blocker",
);
let br_blocked = workspace.run_br(
["create", "Blocked issue", "--json"],
"blocked_shape_blocked",
);
let bd_blocked = workspace.run_bd(
["create", "Blocked issue", "--json"],
"blocked_shape_blocked",
);
assert!(br_blocker.status.success());
assert!(bd_blocker.status.success());
assert!(br_blocked.status.success());
assert!(bd_blocked.status.success());
let br_blocker_id = serde_json::from_str::<Value>(&extract_json_payload(&br_blocker.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("br blocker id");
let bd_blocker_id = serde_json::from_str::<Value>(&extract_json_payload(&bd_blocker.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("bd blocker id");
let br_blocked_id = serde_json::from_str::<Value>(&extract_json_payload(&br_blocked.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("br blocked id");
let bd_blocked_id = serde_json::from_str::<Value>(&extract_json_payload(&bd_blocked.stdout))
.ok()
.and_then(|v| v.get("id").and_then(|id| id.as_str()).map(str::to_string))
.expect("bd blocked id");
let br_dep = workspace.run_br(
["dep", "add", &br_blocked_id, &br_blocker_id],
"blocked_shape_dep",
);
let bd_dep = workspace.run_bd(
["dep", "add", &bd_blocked_id, &bd_blocker_id],
"blocked_shape_dep",
);
assert!(
br_dep.status.success(),
"br dep add failed: {}",
br_dep.stderr
);
assert!(
bd_dep.status.success(),
"bd dep add failed: {}",
bd_dep.stderr
);
let br_blocked_out = workspace.run_br(["blocked", "--json"], "blocked_json_shape");
let bd_blocked_out = workspace.run_bd(["blocked", "--json"], "blocked_json_shape");
assert!(
br_blocked_out.status.success(),
"br blocked failed: {}",
br_blocked_out.stderr
);
assert!(
bd_blocked_out.status.success(),
"bd blocked failed: {}",
bd_blocked_out.stderr
);
let br_json = extract_json_payload(&br_blocked_out.stdout);
let bd_json = extract_json_payload(&bd_blocked_out.stdout);
compare_json(&br_json, &bd_json, &CompareMode::StructureOnly).expect("JSON mismatch");
info!("conformance_blocked_json_shape passed");
}
#[test]
fn conformance_stats() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_stats test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Issue A"], "create_a");
workspace.run_bd(["create", "Issue A"], "create_a");
let br_stats = workspace.run_br(["stats", "--no-activity", "--json"], "stats");
let bd_stats = workspace.run_bd(["stats", "--no-activity", "--json"], "stats");
assert!(
br_stats.status.success(),
"br stats failed: {}",
br_stats.stderr
);
assert!(
bd_stats.status.success(),
"bd stats failed: {}",
bd_stats.stderr
);
let br_json = extract_json_payload(&br_stats.stdout);
let bd_json = extract_json_payload(&bd_stats.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("br json");
let bd_val: Value = serde_json::from_str(&bd_json).expect("bd json");
let br_total = br_val["total"]
.as_i64()
.or_else(|| br_val["summary"]["total"].as_i64());
let bd_total = bd_val["total"]
.as_i64()
.or_else(|| bd_val["summary"]["total"].as_i64());
assert_eq!(
br_total, bd_total,
"total issue counts differ: br={:?}, bd={:?}",
br_total, bd_total
);
info!("conformance_stats passed");
}
#[test]
fn conformance_sync_flush_only() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_flush_only test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Sync test issue"], "create");
workspace.run_bd(["create", "Sync test issue"], "create");
let br_sync = workspace.run_br(["sync", "--flush-only"], "sync");
let bd_sync = workspace.run_bd(["sync", "--flush-only"], "sync");
assert!(
br_sync.status.success(),
"br sync failed: {}",
br_sync.stderr
);
assert!(
bd_sync.status.success(),
"bd sync failed: {}",
bd_sync.stderr
);
let br_jsonl = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_jsonl = workspace.bd_root.join(".beads").join("issues.jsonl");
assert!(br_jsonl.exists(), "br did not create issues.jsonl");
assert!(bd_jsonl.exists(), "bd did not create issues.jsonl");
let br_content = fs::read_to_string(&br_jsonl).expect("read br jsonl");
let bd_content = fs::read_to_string(&bd_jsonl).expect("read bd jsonl");
assert!(!br_content.trim().is_empty(), "br issues.jsonl is empty");
assert!(!bd_content.trim().is_empty(), "bd issues.jsonl is empty");
let br_lines = br_content.lines().count();
let bd_lines = bd_content.lines().count();
assert_eq!(
br_lines, bd_lines,
"JSONL line counts differ: br={}, bd={}",
br_lines, bd_lines
);
info!("conformance_sync_flush_only passed");
}
#[test]
fn conformance_dependency_blocking() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dependency_blocking test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_blocker = workspace.run_br(["create", "Blocker issue", "--json"], "create_blocker");
let bd_blocker = workspace.run_bd(["create", "Blocker issue", "--json"], "create_blocker");
let br_blocked = workspace.run_br(["create", "Blocked issue", "--json"], "create_blocked");
let bd_blocked = workspace.run_bd(["create", "Blocked issue", "--json"], "create_blocked");
let br_blocker_json = extract_json_payload(&br_blocker.stdout);
let bd_blocker_json = extract_json_payload(&bd_blocker.stdout);
let br_blocked_json = extract_json_payload(&br_blocked.stdout);
let bd_blocked_json = extract_json_payload(&bd_blocked.stdout);
let br_blocker_val: Value = serde_json::from_str(&br_blocker_json).expect("parse");
let bd_blocker_val: Value = serde_json::from_str(&bd_blocker_json).expect("parse");
let br_blocked_val: Value = serde_json::from_str(&br_blocked_json).expect("parse");
let bd_blocked_val: Value = serde_json::from_str(&bd_blocked_json).expect("parse");
let br_blocker_id = br_blocker_val["id"]
.as_str()
.or_else(|| br_blocker_val[0]["id"].as_str())
.unwrap();
let bd_blocker_id = bd_blocker_val["id"]
.as_str()
.or_else(|| bd_blocker_val[0]["id"].as_str())
.unwrap();
let br_blocked_id = br_blocked_val["id"]
.as_str()
.or_else(|| br_blocked_val[0]["id"].as_str())
.unwrap();
let bd_blocked_id = bd_blocked_val["id"]
.as_str()
.or_else(|| bd_blocked_val[0]["id"].as_str())
.unwrap();
let br_dep = workspace.run_br(["dep", "add", br_blocked_id, br_blocker_id], "add_dep");
let bd_dep = workspace.run_bd(["dep", "add", bd_blocked_id, bd_blocker_id], "add_dep");
assert!(
br_dep.status.success(),
"br dep add failed: {}",
br_dep.stderr
);
assert!(
bd_dep.status.success(),
"bd dep add failed: {}",
bd_dep.stderr
);
let br_blocked_cmd = workspace.run_br(["blocked", "--json"], "blocked");
let bd_blocked_cmd = workspace.run_bd(["blocked", "--json"], "blocked");
assert!(br_blocked_cmd.status.success(), "br blocked failed");
assert!(bd_blocked_cmd.status.success(), "bd blocked failed");
let br_blocked_json = extract_json_payload(&br_blocked_cmd.stdout);
let bd_blocked_json = extract_json_payload(&bd_blocked_cmd.stdout);
let br_val: Value = serde_json::from_str(&br_blocked_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_blocked_json).unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"blocked counts differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 1, "expected 1 blocked issue");
let br_ready = workspace.run_br(["ready", "--json"], "ready_after_dep");
let bd_ready = workspace.run_bd(["ready", "--json"], "ready_after_dep");
let br_ready_json = extract_json_payload(&br_ready.stdout);
let bd_ready_json = extract_json_payload(&bd_ready.stdout);
let br_ready_val: Value = serde_json::from_str(&br_ready_json).unwrap_or(Value::Array(vec![]));
let bd_ready_val: Value = serde_json::from_str(&bd_ready_json).unwrap_or(Value::Array(vec![]));
let br_ready_len = br_ready_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_ready_len = bd_ready_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_ready_len, bd_ready_len,
"ready counts differ: br={}, bd={}",
br_ready_len, bd_ready_len
);
assert_eq!(br_ready_len, 1, "expected 1 ready issue (the blocker)");
info!("conformance_dependency_blocking passed");
}
#[test]
fn conformance_close_issue() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_close_issue test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Issue to close", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Issue to close", "--json"], "create");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
let br_close = workspace.run_br(["close", br_id, "--json"], "close");
let bd_close = workspace.run_bd(["close", bd_id, "--json"], "close");
assert!(
br_close.status.success(),
"br close failed: {}",
br_close.stderr
);
assert!(
bd_close.status.success(),
"bd close failed: {}",
bd_close.stderr
);
let br_show = workspace.run_br(["show", br_id, "--json"], "show_after_close");
let bd_show = workspace.run_bd(["show", bd_id, "--json"], "show_after_close");
assert!(
br_show.status.success(),
"br show failed: {}",
br_show.stderr
);
assert!(
bd_show.status.success(),
"bd show failed: {}",
bd_show.stderr
);
let br_show_json = extract_json_payload(&br_show.stdout);
let bd_show_json = extract_json_payload(&bd_show.stdout);
let br_show_val: Value = serde_json::from_str(&br_show_json).expect("parse");
let bd_show_val: Value = serde_json::from_str(&bd_show_json).expect("parse");
let br_issue = if br_show_val.is_array() {
&br_show_val[0]
} else {
&br_show_val
};
let bd_issue = if bd_show_val.is_array() {
&bd_show_val[0]
} else {
&bd_show_val
};
assert_eq!(
br_issue["status"].as_str(),
Some("closed"),
"br issue not closed: got {:?}",
br_issue["status"]
);
assert_eq!(
bd_issue["status"].as_str(),
Some("closed"),
"bd issue not closed: got {:?}",
bd_issue["status"]
);
info!("conformance_close_issue passed");
}
#[test]
fn conformance_update_issue() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_update_issue test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Issue to update", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Issue to update", "--json"], "create");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
let br_update = workspace.run_br(
["update", br_id, "--priority", "0", "--json"],
"update_priority",
);
let bd_update = workspace.run_bd(
["update", bd_id, "--priority", "0", "--json"],
"update_priority",
);
assert!(
br_update.status.success(),
"br update failed: {}",
br_update.stderr
);
assert!(
bd_update.status.success(),
"bd update failed: {}",
bd_update.stderr
);
let br_show = workspace.run_br(["show", br_id, "--json"], "show_after_update");
let bd_show = workspace.run_bd(["show", bd_id, "--json"], "show_after_update");
let br_show_json = extract_json_payload(&br_show.stdout);
let bd_show_json = extract_json_payload(&bd_show.stdout);
let br_show_val: Value = serde_json::from_str(&br_show_json).expect("parse");
let bd_show_val: Value = serde_json::from_str(&bd_show_json).expect("parse");
let br_priority = br_show_val["priority"]
.as_i64()
.or_else(|| br_show_val[0]["priority"].as_i64());
let bd_priority = bd_show_val["priority"]
.as_i64()
.or_else(|| bd_show_val[0]["priority"].as_i64());
assert_eq!(
br_priority, bd_priority,
"priority mismatch after update: br={:?}, bd={:?}",
br_priority, bd_priority
);
assert_eq!(br_priority, Some(0), "expected priority 0");
info!("conformance_update_issue passed");
}
#[test]
fn conformance_reopen_basic() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_reopen_basic test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Issue to reopen", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Issue to reopen", "--json"], "create");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
workspace.run_br(["close", br_id], "close");
workspace.run_bd(["close", bd_id], "close");
let br_reopen = workspace.run_br(["reopen", br_id, "--json"], "reopen");
let bd_reopen = workspace.run_bd(["reopen", bd_id, "--json"], "reopen");
assert!(
br_reopen.status.success(),
"br reopen failed: {}",
br_reopen.stderr
);
assert!(
bd_reopen.status.success(),
"bd reopen failed: {}",
bd_reopen.stderr
);
let br_show = workspace.run_br(["show", br_id, "--json"], "show_after_reopen");
let bd_show = workspace.run_bd(["show", bd_id, "--json"], "show_after_reopen");
let br_show_json = extract_json_payload(&br_show.stdout);
let bd_show_json = extract_json_payload(&bd_show.stdout);
let br_show_val: Value = serde_json::from_str(&br_show_json).expect("parse");
let bd_show_val: Value = serde_json::from_str(&bd_show_json).expect("parse");
let br_status = br_show_val["status"]
.as_str()
.or_else(|| br_show_val[0]["status"].as_str());
let bd_status = bd_show_val["status"]
.as_str()
.or_else(|| bd_show_val[0]["status"].as_str());
assert_eq!(
br_status, bd_status,
"status mismatch after reopen: br={:?}, bd={:?}",
br_status, bd_status
);
assert_eq!(br_status, Some("open"), "expected status open");
info!("conformance_reopen_basic passed");
}
#[test]
#[ignore = "bd v0.46.0 list --type filter returns empty: known behavioral difference"]
fn conformance_list_by_type() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_list_by_type test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Bug issue", "--type", "bug"], "create_bug");
workspace.run_br_in_bd_env(["create", "Bug issue", "--type", "bug"], "create_bug");
workspace.run_br(
["create", "Feature issue", "--type", "feature"],
"create_feature",
);
workspace.run_br_in_bd_env(
["create", "Feature issue", "--type", "feature"],
"create_feature",
);
workspace.run_br(["create", "Task issue", "--type", "task"], "create_task");
workspace.run_br_in_bd_env(["create", "Task issue", "--type", "task"], "create_task");
let br_list = workspace.run_br(["list", "--type", "bug", "--json"], "list_bugs");
let bd_list = workspace.run_bd(["list", "--type", "bug", "--json"], "list_bugs");
assert!(
br_list.status.success(),
"br list failed: {}",
br_list.stderr
);
assert!(
bd_list.status.success(),
"bd list failed: {}",
bd_list.stderr
);
let br_json = extract_json_payload(&br_list.stdout);
let bd_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"bug list lengths differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 1, "expected exactly 1 bug");
info!("conformance_list_by_type passed");
}
#[test]
fn conformance_show_basic() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_show_basic test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(
[
"create",
"Show test issue",
"--type",
"task",
"--priority",
"2",
"--json",
],
"create",
);
let bd_create = workspace.run_bd(
[
"create",
"Show test issue",
"--type",
"task",
"--priority",
"2",
"--json",
],
"create",
);
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
let br_show = workspace.run_br(["show", br_id, "--json"], "show");
let bd_show = workspace.run_bd(["show", bd_id, "--json"], "show");
assert!(
br_show.status.success(),
"br show failed: {}",
br_show.stderr
);
assert!(
bd_show.status.success(),
"bd show failed: {}",
bd_show.stderr
);
let br_show_json = extract_json_payload(&br_show.stdout);
let bd_show_json = extract_json_payload(&bd_show.stdout);
let result = compare_json(
&br_show_json,
&bd_show_json,
&CompareMode::ContainsFields(vec![
"title".to_string(),
"status".to_string(),
"issue_type".to_string(),
"priority".to_string(),
]),
);
assert!(
result.is_ok(),
"show JSON comparison failed: {:?}",
result.err()
);
info!("conformance_show_basic passed");
}
#[test]
fn conformance_search_basic() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_search_basic test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Authentication bug in login"], "create1");
workspace.run_bd(["create", "Authentication bug in login"], "create1");
workspace.run_br(["create", "Payment processing feature"], "create2");
workspace.run_bd(["create", "Payment processing feature"], "create2");
workspace.run_br(["create", "User login flow improvement"], "create3");
workspace.run_bd(["create", "User login flow improvement"], "create3");
let br_search = workspace.run_br(["search", "login", "--json"], "search_login");
let bd_search = workspace.run_bd(["search", "login", "--json"], "search_login");
assert!(
br_search.status.success(),
"br search failed: {}",
br_search.stderr
);
assert!(
bd_search.status.success(),
"bd search failed: {}",
bd_search.stderr
);
let br_json = extract_json_payload(&br_search.stdout);
let bd_json = extract_json_payload(&bd_search.stdout);
let br_val: Value = serde_json::from_str(&br_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_json).unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"search result lengths differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 2, "expected 2 issues matching 'login'");
info!("conformance_search_basic passed");
}
#[test]
fn conformance_label_basic() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_label_basic test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Issue for labels", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Issue for labels", "--json"], "create");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
let br_add = workspace.run_br(["label", "add", br_id, "urgent"], "label_add");
let bd_add = workspace.run_bd(["label", "add", bd_id, "urgent"], "label_add");
assert!(
br_add.status.success(),
"br label add failed: {}",
br_add.stderr
);
assert!(
bd_add.status.success(),
"bd label add failed: {}",
bd_add.stderr
);
let br_list = workspace.run_br(["label", "list", br_id, "--json"], "label_list");
let bd_list = workspace.run_bd(["label", "list", bd_id, "--json"], "label_list");
assert!(
br_list.status.success(),
"br label list failed: {}",
br_list.stderr
);
assert!(
bd_list.status.success(),
"bd label list failed: {}",
bd_list.stderr
);
let br_label_json = extract_json_payload(&br_list.stdout);
let bd_label_json = extract_json_payload(&bd_list.stdout);
assert!(
br_label_json.contains("urgent"),
"br missing 'urgent' label: {}",
br_label_json
);
assert!(
bd_label_json.contains("urgent"),
"bd missing 'urgent' label: {}",
bd_label_json
);
info!("conformance_label_basic passed");
}
#[test]
fn conformance_dep_list() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_list test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_parent = workspace.run_br(["create", "Parent issue", "--json"], "create_parent");
let bd_parent = workspace.run_bd(["create", "Parent issue", "--json"], "create_parent");
let br_child = workspace.run_br(["create", "Child issue", "--json"], "create_child");
let bd_child = workspace.run_bd(["create", "Child issue", "--json"], "create_child");
let br_parent_json = extract_json_payload(&br_parent.stdout);
let bd_parent_json = extract_json_payload(&bd_parent.stdout);
let br_child_json = extract_json_payload(&br_child.stdout);
let bd_child_json = extract_json_payload(&bd_child.stdout);
let br_parent_val: Value = serde_json::from_str(&br_parent_json).expect("parse");
let bd_parent_val: Value = serde_json::from_str(&bd_parent_json).expect("parse");
let br_child_val: Value = serde_json::from_str(&br_child_json).expect("parse");
let bd_child_val: Value = serde_json::from_str(&bd_child_json).expect("parse");
let br_parent_id = br_parent_val["id"]
.as_str()
.or_else(|| br_parent_val[0]["id"].as_str())
.unwrap();
let bd_parent_id = bd_parent_val["id"]
.as_str()
.or_else(|| bd_parent_val[0]["id"].as_str())
.unwrap();
let br_child_id = br_child_val["id"]
.as_str()
.or_else(|| br_child_val[0]["id"].as_str())
.unwrap();
let bd_child_id = bd_child_val["id"]
.as_str()
.or_else(|| bd_child_val[0]["id"].as_str())
.unwrap();
let br_dep = workspace.run_br(["dep", "add", br_child_id, br_parent_id], "dep_add");
let bd_dep = workspace.run_bd(["dep", "add", bd_child_id, bd_parent_id], "dep_add");
assert!(
br_dep.status.success(),
"br dep add failed: {}",
br_dep.stderr
);
assert!(
bd_dep.status.success(),
"bd dep add failed: {}",
bd_dep.stderr
);
let br_list = workspace.run_br(["dep", "list", br_child_id, "--json"], "dep_list");
let bd_list = workspace.run_bd(["dep", "list", bd_child_id, "--json"], "dep_list");
assert!(
br_list.status.success(),
"br dep list failed: {}",
br_list.stderr
);
assert!(
bd_list.status.success(),
"bd dep list failed: {}",
bd_list.stderr
);
let br_dep_json = extract_json_payload(&br_list.stdout);
let bd_dep_json = extract_json_payload(&bd_list.stdout);
let br_dep_val: Value = serde_json::from_str(&br_dep_json).unwrap_or(Value::Array(vec![]));
let bd_dep_val: Value = serde_json::from_str(&bd_dep_json).unwrap_or(Value::Array(vec![]));
let br_dep_len = br_dep_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_dep_len = bd_dep_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_dep_len, bd_dep_len,
"dep list lengths differ: br={}, bd={}",
br_dep_len, bd_dep_len
);
assert_eq!(br_dep_len, 1, "expected 1 dependency");
info!("conformance_dep_list passed");
}
#[test]
fn conformance_count_basic() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_count_basic test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let _br_create1 = workspace.run_br(["create", "Open issue 1", "--json"], "create1");
let _bd_create1 = workspace.run_bd(["create", "Open issue 1", "--json"], "create1");
let _br_create2 = workspace.run_br(["create", "Open issue 2", "--json"], "create2");
let _bd_create2 = workspace.run_bd(["create", "Open issue 2", "--json"], "create2");
let br_create3 = workspace.run_br(["create", "Will close", "--json"], "create3");
let bd_create3 = workspace.run_bd(["create", "Will close", "--json"], "create3");
let br_json = extract_json_payload(&br_create3.stdout);
let bd_json = extract_json_payload(&bd_create3.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
workspace.run_br(["close", br_id], "close");
workspace.run_bd(["close", bd_id], "close");
let br_count = workspace.run_br(["count", "--json"], "count");
let bd_count = workspace.run_bd(["count", "--json"], "count");
assert!(
br_count.status.success(),
"br count failed: {}",
br_count.stderr
);
assert!(
bd_count.status.success(),
"bd count failed: {}",
bd_count.stderr
);
let br_count_json = extract_json_payload(&br_count.stdout);
let bd_count_json = extract_json_payload(&bd_count.stdout);
let br_count_val: Value = serde_json::from_str(&br_count_json).expect("parse");
let bd_count_val: Value = serde_json::from_str(&bd_count_json).expect("parse");
let br_total = br_count_val["total"]
.as_i64()
.or_else(|| br_count_val["summary"]["total"].as_i64());
let bd_total = bd_count_val["total"]
.as_i64()
.or_else(|| bd_count_val["summary"]["total"].as_i64());
assert_eq!(
br_total, bd_total,
"total counts differ: br={:?}, bd={:?}",
br_total, bd_total
);
info!("conformance_count_basic passed");
}
#[test]
fn conformance_delete_issue() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_delete_issue test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Issue to delete", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Issue to delete", "--json"], "create");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
let br_delete = workspace.run_br(["delete", br_id, "--reason", "test deletion"], "delete");
let bd_delete = workspace.run_bd(
["delete", bd_id, "--reason", "test deletion", "--force"],
"delete",
);
assert!(
br_delete.status.success(),
"br delete failed: {}",
br_delete.stderr
);
assert!(
bd_delete.status.success(),
"bd delete failed: {}",
bd_delete.stderr
);
let br_list = workspace.run_br(["list", "--json"], "list_after_delete");
let bd_list = workspace.run_bd(["list", "--json"], "list_after_delete");
let br_list_json = extract_json_payload(&br_list.stdout);
let bd_list_json = extract_json_payload(&bd_list.stdout);
let br_list_val: Value = serde_json::from_str(&br_list_json).unwrap_or(Value::Array(vec![]));
let bd_list_val: Value = serde_json::from_str(&bd_list_json).unwrap_or(Value::Array(vec![]));
let br_len = br_list_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_list_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"list lengths differ after delete: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 0, "expected empty list after deletion");
info!("conformance_delete_issue passed");
}
#[test]
#[ignore]
fn conformance_delete_creates_tombstone() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_delete_creates_tombstone test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Tombstone issue", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Tombstone issue", "--json"], "create");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
workspace.run_br(["delete", br_id, "--reason", "cleanup"], "delete");
workspace.run_bd(
["delete", bd_id, "--reason", "cleanup", "--force"],
"delete",
);
let br_show = workspace.run_br(["show", br_id, "--json"], "show_tombstone");
let bd_show = workspace.run_bd(["show", bd_id, "--json"], "show_tombstone");
assert_eq!(
br_show.status.success(),
bd_show.status.success(),
"show tombstone behavior differs: br success={}, bd success={}",
br_show.status.success(),
bd_show.status.success()
);
if br_show.status.success() && bd_show.status.success() {
let br_show_json = extract_json_payload(&br_show.stdout);
let bd_show_json = extract_json_payload(&bd_show.stdout);
if br_show_json.trim().is_empty() || bd_show_json.trim().is_empty() {
assert!(
br_show_json.trim().is_empty() && bd_show_json.trim().is_empty(),
"tombstone show output mismatch: br='{}' bd='{}'",
br_show_json,
bd_show_json
);
} else {
let br_val: Value = serde_json::from_str(&br_show_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_show_json).expect("parse");
let br_issue = if br_val.is_array() {
&br_val[0]
} else {
&br_val
};
let bd_issue = if bd_val.is_array() {
&bd_val[0]
} else {
&bd_val
};
assert_eq!(
br_issue["status"].as_str(),
bd_issue["status"].as_str(),
"tombstone status mismatch"
);
}
}
info!("conformance_delete_creates_tombstone passed");
}
#[test]
fn conformance_delete_already_deleted_error() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_delete_already_deleted_error test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Delete twice", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Delete twice", "--json"], "create");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
workspace.run_br(["delete", br_id, "--reason", "cleanup"], "delete_first");
workspace.run_bd(
["delete", bd_id, "--reason", "cleanup", "--force"],
"delete_first",
);
let br_delete = workspace.run_br(["delete", br_id, "--reason", "cleanup"], "delete_second");
let bd_delete = workspace.run_bd(
["delete", bd_id, "--reason", "cleanup", "--force"],
"delete_second",
);
assert_eq!(
br_delete.status.success(),
bd_delete.status.success(),
"delete already deleted behavior differs: br success={}, bd success={}",
br_delete.status.success(),
bd_delete.status.success()
);
info!("conformance_delete_already_deleted_error passed");
}
#[test]
fn conformance_delete_with_dependents() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_delete_with_dependents test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_parent = workspace.run_br(["create", "Parent issue", "--json"], "create_parent");
let bd_parent = workspace.run_bd(["create", "Parent issue", "--json"], "create_parent");
let br_child = workspace.run_br(["create", "Child issue", "--json"], "create_child");
let bd_child = workspace.run_bd(["create", "Child issue", "--json"], "create_child");
let br_parent_id = extract_issue_id(&extract_json_payload(&br_parent.stdout));
let bd_parent_id = extract_issue_id(&extract_json_payload(&bd_parent.stdout));
let br_child_id = extract_issue_id(&extract_json_payload(&br_child.stdout));
let bd_child_id = extract_issue_id(&extract_json_payload(&bd_child.stdout));
workspace.run_br(["dep", "add", &br_child_id, &br_parent_id], "dep_add");
workspace.run_bd(["dep", "add", &bd_child_id, &bd_parent_id], "dep_add");
workspace.run_br(
["delete", &br_parent_id, "--reason", "cleanup"],
"delete_parent",
);
workspace.run_bd(
["delete", &bd_parent_id, "--reason", "cleanup", "--force"],
"delete_parent",
);
let br_show = workspace.run_br(["show", &br_child_id, "--json"], "show_child");
let bd_show = workspace.run_bd(["show", &bd_child_id, "--json"], "show_child");
assert_eq!(
br_show.status.success(),
bd_show.status.success(),
"child visibility differs after parent delete: br success={}, bd success={}",
br_show.status.success(),
bd_show.status.success()
);
info!("conformance_delete_with_dependents passed");
}
#[test]
fn conformance_dep_remove() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_remove test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_blocker = workspace.run_br(["create", "Blocker", "--json"], "create_blocker");
let bd_blocker = workspace.run_bd(["create", "Blocker", "--json"], "create_blocker");
let br_blocked = workspace.run_br(["create", "Blocked", "--json"], "create_blocked");
let bd_blocked = workspace.run_bd(["create", "Blocked", "--json"], "create_blocked");
let br_blocker_id = {
let json = extract_json_payload(&br_blocker.stdout);
let val: Value = serde_json::from_str(&json).expect("parse");
val["id"]
.as_str()
.or_else(|| val[0]["id"].as_str())
.unwrap()
.to_string()
};
let bd_blocker_id = {
let json = extract_json_payload(&bd_blocker.stdout);
let val: Value = serde_json::from_str(&json).expect("parse");
val["id"]
.as_str()
.or_else(|| val[0]["id"].as_str())
.unwrap()
.to_string()
};
let br_blocked_id = {
let json = extract_json_payload(&br_blocked.stdout);
let val: Value = serde_json::from_str(&json).expect("parse");
val["id"]
.as_str()
.or_else(|| val[0]["id"].as_str())
.unwrap()
.to_string()
};
let bd_blocked_id = {
let json = extract_json_payload(&bd_blocked.stdout);
let val: Value = serde_json::from_str(&json).expect("parse");
val["id"]
.as_str()
.or_else(|| val[0]["id"].as_str())
.unwrap()
.to_string()
};
workspace.run_br(["dep", "add", &br_blocked_id, &br_blocker_id], "add_dep");
workspace.run_bd(["dep", "add", &bd_blocked_id, &bd_blocker_id], "add_dep");
let br_blocked_cmd = workspace.run_br(["blocked", "--json"], "blocked_before");
let bd_blocked_cmd = workspace.run_bd(["blocked", "--json"], "blocked_before");
let br_before_json = extract_json_payload(&br_blocked_cmd.stdout);
let bd_before_json = extract_json_payload(&bd_blocked_cmd.stdout);
let br_before: Value = serde_json::from_str(&br_before_json).unwrap_or(Value::Array(vec![]));
let bd_before: Value = serde_json::from_str(&bd_before_json).unwrap_or(Value::Array(vec![]));
assert_eq!(
br_before.as_array().map(|a| a.len()).unwrap_or(0),
1,
"expected 1 blocked issue before remove"
);
assert_eq!(
bd_before.as_array().map(|a| a.len()).unwrap_or(0),
1,
"expected 1 blocked issue before remove"
);
let br_rm = workspace.run_br(["dep", "remove", &br_blocked_id, &br_blocker_id], "rm_dep");
let bd_rm = workspace.run_bd(["dep", "remove", &bd_blocked_id, &bd_blocker_id], "rm_dep");
assert!(
br_rm.status.success(),
"br dep remove failed: {}",
br_rm.stderr
);
assert!(
bd_rm.status.success(),
"bd dep remove failed: {}",
bd_rm.stderr
);
let br_blocked_after = workspace.run_br(["blocked", "--json"], "blocked_after");
let bd_blocked_after = workspace.run_bd(["blocked", "--json"], "blocked_after");
let br_after_json = extract_json_payload(&br_blocked_after.stdout);
let bd_after_json = extract_json_payload(&bd_blocked_after.stdout);
let br_after: Value = serde_json::from_str(&br_after_json).unwrap_or(Value::Array(vec![]));
let bd_after: Value = serde_json::from_str(&bd_after_json).unwrap_or(Value::Array(vec![]));
let br_len = br_after.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_after.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"blocked counts differ after remove: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 0, "expected no blocked issues after dep remove");
info!("conformance_dep_remove passed");
}
#[test]
fn conformance_sync_import() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_import test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Import test A"], "create_a");
workspace.run_bd(["create", "Import test A"], "create_a");
workspace.run_br(["create", "Import test B"], "create_b");
workspace.run_bd(["create", "Import test B"], "create_b");
workspace.run_br(["sync", "--flush-only"], "export");
workspace.run_bd(["sync", "--flush-only"], "export");
let import_workspace = ConformanceWorkspace::new();
import_workspace.init_both();
let br_src_jsonl = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_src_jsonl = workspace.bd_root.join(".beads").join("issues.jsonl");
let br_dst_jsonl = import_workspace.br_root.join(".beads").join("issues.jsonl");
let bd_dst_jsonl = import_workspace.bd_root.join(".beads").join("issues.jsonl");
fs::copy(&br_src_jsonl, &br_dst_jsonl).expect("copy br jsonl");
fs::copy(&bd_src_jsonl, &bd_dst_jsonl).expect("copy bd jsonl");
let br_import = import_workspace.run_br(["sync", "--import-only"], "import");
let bd_import = import_workspace.run_bd(["sync", "--import-only"], "import");
assert!(
br_import.status.success(),
"br import failed: {}",
br_import.stderr
);
assert!(
bd_import.status.success(),
"bd import failed: {}",
bd_import.stderr
);
let br_list = import_workspace.run_br(["list", "--json"], "list_after_import");
let bd_list = import_workspace.run_bd(["list", "--json"], "list_after_import");
let br_json = extract_json_payload(&br_list.stdout);
let bd_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_json).unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"import counts differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 2, "expected 2 issues after import");
info!("conformance_sync_import passed");
}
#[test]
fn conformance_sync_roundtrip() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_roundtrip test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(
[
"create",
"Roundtrip bug",
"--type",
"bug",
"--priority",
"1",
],
"create_bug",
);
workspace.run_bd(
[
"create",
"Roundtrip bug",
"--type",
"bug",
"--priority",
"1",
],
"create_bug",
);
workspace.run_br(
[
"create",
"Roundtrip feature",
"--type",
"feature",
"--priority",
"3",
],
"create_feature",
);
workspace.run_bd(
[
"create",
"Roundtrip feature",
"--type",
"feature",
"--priority",
"3",
],
"create_feature",
);
workspace.run_br(["sync", "--flush-only"], "export");
workspace.run_bd(["sync", "--flush-only"], "export");
let br_jsonl_path = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_jsonl_path = workspace.bd_root.join(".beads").join("issues.jsonl");
let br_jsonl = fs::read_to_string(&br_jsonl_path).expect("read br jsonl");
let bd_jsonl = fs::read_to_string(&bd_jsonl_path).expect("read bd jsonl");
let br_lines = br_jsonl.lines().count();
let bd_lines = bd_jsonl.lines().count();
assert_eq!(
br_lines, bd_lines,
"JSONL line counts differ: br={}, bd={}",
br_lines, bd_lines
);
assert_eq!(br_lines, 2, "expected 2 lines in JSONL");
let br_titles: HashSet<String> = br_jsonl
.lines()
.map(|line| {
let val: Value = serde_json::from_str(line).expect("parse br line");
val["title"].as_str().unwrap_or("").to_string()
})
.collect();
let bd_titles: HashSet<String> = bd_jsonl
.lines()
.map(|line| {
let val: Value = serde_json::from_str(line).expect("parse bd line");
val["title"].as_str().unwrap_or("").to_string()
})
.collect();
assert_eq!(
br_titles, bd_titles,
"JSONL titles differ: br={:?}, bd={:?}",
br_titles, bd_titles
);
let import_workspace = ConformanceWorkspace::new();
import_workspace.init_both();
let br_dst_jsonl = import_workspace.br_root.join(".beads").join("issues.jsonl");
let bd_dst_jsonl = import_workspace.bd_root.join(".beads").join("issues.jsonl");
fs::copy(&br_jsonl_path, &br_dst_jsonl).expect("copy br jsonl");
fs::copy(&bd_jsonl_path, &bd_dst_jsonl).expect("copy bd jsonl");
import_workspace.run_br(["sync", "--import-only"], "import");
import_workspace.run_bd(["sync", "--import-only"], "import");
let br_after = import_workspace.run_br(["list", "--json"], "list_after");
let bd_after = import_workspace.run_bd(["list", "--json"], "list_after");
let br_after_json = extract_json_payload(&br_after.stdout);
let bd_after_json = extract_json_payload(&bd_after.stdout);
let br_after_val: Value = serde_json::from_str(&br_after_json).expect("parse");
let bd_after_val: Value = serde_json::from_str(&bd_after_json).expect("parse");
let br_after_len = br_after_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_after_len = bd_after_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_after_len, bd_after_len,
"roundtrip counts differ: br={}, bd={}",
br_after_len, bd_after_len
);
assert_eq!(br_after_len, 2, "expected 2 issues after roundtrip");
info!("conformance_sync_roundtrip passed");
}
#[test]
fn conformance_sync_flush_empty_db() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_flush_empty_db test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_sync = workspace.run_br(["sync", "--flush-only"], "flush_empty");
let bd_sync = workspace.run_bd(["sync", "--flush-only"], "flush_empty");
assert_eq!(
br_sync.status.success(),
bd_sync.status.success(),
"flush empty behavior differs: br={}, bd={}",
br_sync.status.success(),
bd_sync.status.success()
);
if br_sync.status.success() {
let br_jsonl = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_jsonl = workspace.bd_root.join(".beads").join("issues.jsonl");
if br_jsonl.exists() && bd_jsonl.exists() {
let br_content = fs::read_to_string(&br_jsonl).unwrap_or_default();
let bd_content = fs::read_to_string(&bd_jsonl).unwrap_or_default();
let br_lines = br_content.lines().filter(|l| !l.is_empty()).count();
let bd_lines = bd_content.lines().filter(|l| !l.is_empty()).count();
assert_eq!(
br_lines, bd_lines,
"empty db JSONL line counts differ: br={}, bd={}",
br_lines, bd_lines
);
}
}
info!("conformance_sync_flush_empty_db passed");
}
#[test]
fn conformance_sync_flush_single_issue() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_flush_single_issue test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Single issue for sync"], "create");
workspace.run_bd(["create", "Single issue for sync"], "create");
let br_sync = workspace.run_br(["sync", "--flush-only"], "flush");
let bd_sync = workspace.run_bd(["sync", "--flush-only"], "flush");
assert!(br_sync.status.success(), "br flush failed");
assert!(bd_sync.status.success(), "bd flush failed");
let br_jsonl = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_jsonl = workspace.bd_root.join(".beads").join("issues.jsonl");
let br_content = fs::read_to_string(&br_jsonl).expect("read br jsonl");
let bd_content = fs::read_to_string(&bd_jsonl).expect("read bd jsonl");
let br_lines: Vec<&str> = br_content.lines().filter(|l| !l.is_empty()).collect();
let bd_lines: Vec<&str> = bd_content.lines().filter(|l| !l.is_empty()).collect();
assert_eq!(br_lines.len(), 1, "br should have 1 line");
assert_eq!(bd_lines.len(), 1, "bd should have 1 line");
let br_val: Value = serde_json::from_str(br_lines[0]).expect("parse br jsonl");
let bd_val: Value = serde_json::from_str(bd_lines[0]).expect("parse bd jsonl");
assert_eq!(
br_val["title"].as_str(),
bd_val["title"].as_str(),
"titles should match"
);
info!("conformance_sync_flush_single_issue passed");
}
#[test]
fn conformance_sync_flush_many_issues() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_flush_many_issues test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
for i in 0..20 {
workspace.run_br(
["create", &format!("Issue number {}", i)],
&format!("create_{}", i),
);
workspace.run_bd(
["create", &format!("Issue number {}", i)],
&format!("create_{}", i),
);
}
let br_sync = workspace.run_br(["sync", "--flush-only"], "flush");
let bd_sync = workspace.run_bd(["sync", "--flush-only"], "flush");
assert!(br_sync.status.success(), "br flush failed");
assert!(bd_sync.status.success(), "bd flush failed");
let br_jsonl = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_jsonl = workspace.bd_root.join(".beads").join("issues.jsonl");
let br_content = fs::read_to_string(&br_jsonl).expect("read br jsonl");
let bd_content = fs::read_to_string(&bd_jsonl).expect("read bd jsonl");
let br_lines = br_content.lines().filter(|l| !l.is_empty()).count();
let bd_lines = bd_content.lines().filter(|l| !l.is_empty()).count();
assert_eq!(
br_lines, bd_lines,
"many issues JSONL line counts differ: br={}, bd={}",
br_lines, bd_lines
);
assert_eq!(br_lines, 20, "expected 20 lines in JSONL");
info!("conformance_sync_flush_many_issues passed");
}
#[test]
fn conformance_sync_flush_with_dependencies() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_flush_with_dependencies test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_blocker = workspace.run_br(["create", "Blocker issue", "--json"], "create_blocker");
let bd_blocker = workspace.run_bd(["create", "Blocker issue", "--json"], "create_blocker");
let br_blocked = workspace.run_br(["create", "Blocked issue", "--json"], "create_blocked");
let bd_blocked = workspace.run_bd(["create", "Blocked issue", "--json"], "create_blocked");
let br_blocker_id = extract_issue_id(&extract_json_payload(&br_blocker.stdout));
let bd_blocker_id = extract_issue_id(&extract_json_payload(&bd_blocker.stdout));
let br_blocked_id = extract_issue_id(&extract_json_payload(&br_blocked.stdout));
let bd_blocked_id = extract_issue_id(&extract_json_payload(&bd_blocked.stdout));
workspace.run_br(["dep", "add", &br_blocked_id, &br_blocker_id], "add_dep");
workspace.run_bd(["dep", "add", &bd_blocked_id, &bd_blocker_id], "add_dep");
let br_sync = workspace.run_br(["sync", "--flush-only"], "flush");
let bd_sync = workspace.run_bd(["sync", "--flush-only"], "flush");
assert!(br_sync.status.success(), "br flush failed");
assert!(bd_sync.status.success(), "bd flush failed");
let br_jsonl = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_jsonl = workspace.bd_root.join(".beads").join("issues.jsonl");
let br_content = fs::read_to_string(&br_jsonl).expect("read br jsonl");
let bd_content = fs::read_to_string(&bd_jsonl).expect("read bd jsonl");
let br_lines = br_content.lines().filter(|l| !l.is_empty()).count();
let bd_lines = bd_content.lines().filter(|l| !l.is_empty()).count();
assert_eq!(br_lines, 2, "br should have 2 lines");
assert_eq!(bd_lines, 2, "bd should have 2 lines");
info!(
"br JSONL size: {}, bd JSONL size: {}",
br_content.len(),
bd_content.len()
);
info!("conformance_sync_flush_with_dependencies passed");
}
#[test]
fn conformance_sync_flush_with_labels() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_flush_with_labels test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_issue = workspace.run_br(["create", "Labeled issue", "--json"], "create");
let bd_issue = workspace.run_bd(["create", "Labeled issue", "--json"], "create");
let br_id = extract_issue_id(&extract_json_payload(&br_issue.stdout));
let bd_id = extract_issue_id(&extract_json_payload(&bd_issue.stdout));
workspace.run_br(["label", "add", &br_id, "test-label"], "add_label");
workspace.run_bd(["label", "add", &bd_id, "test-label"], "add_label");
let br_sync = workspace.run_br(["sync", "--flush-only"], "flush");
let bd_sync = workspace.run_bd(["sync", "--flush-only"], "flush");
assert!(br_sync.status.success(), "br flush failed");
assert!(bd_sync.status.success(), "bd flush failed");
let br_jsonl = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_jsonl = workspace.bd_root.join(".beads").join("issues.jsonl");
let br_content = fs::read_to_string(&br_jsonl).expect("read br jsonl");
let bd_content = fs::read_to_string(&bd_jsonl).expect("read bd jsonl");
let br_val: Value = serde_json::from_str(br_content.lines().next().unwrap()).expect("parse");
let bd_val: Value = serde_json::from_str(bd_content.lines().next().unwrap()).expect("parse");
let br_has_labels = br_val.get("labels").is_some();
let bd_has_labels = bd_val.get("labels").is_some();
info!(
"Labels in JSONL: br={}, bd={}",
br_has_labels, bd_has_labels
);
info!("conformance_sync_flush_with_labels passed");
}
#[test]
fn conformance_sync_flush_jsonl_line_format() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_flush_jsonl_line_format test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(
[
"create",
"Format test issue",
"--type",
"bug",
"--priority",
"1",
],
"create",
);
workspace.run_bd(
[
"create",
"Format test issue",
"--type",
"bug",
"--priority",
"1",
],
"create",
);
workspace.run_br(["sync", "--flush-only"], "flush");
workspace.run_bd(["sync", "--flush-only"], "flush");
let br_jsonl = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_jsonl = workspace.bd_root.join(".beads").join("issues.jsonl");
let br_content = fs::read_to_string(&br_jsonl).expect("read br jsonl");
let bd_content = fs::read_to_string(&bd_jsonl).expect("read bd jsonl");
for (i, line) in br_content.lines().filter(|l| !l.is_empty()).enumerate() {
serde_json::from_str::<Value>(line)
.unwrap_or_else(|e| panic!("br JSONL line {} is not valid JSON: {}", i, e));
}
for (i, line) in bd_content.lines().filter(|l| !l.is_empty()).enumerate() {
serde_json::from_str::<Value>(line)
.unwrap_or_else(|e| panic!("bd JSONL line {} is not valid JSON: {}", i, e));
}
let br_val: Value = serde_json::from_str(br_content.lines().next().unwrap()).expect("parse br");
let bd_val: Value = serde_json::from_str(bd_content.lines().next().unwrap()).expect("parse bd");
let required_fields = ["id", "title", "status", "priority"];
for field in required_fields {
assert!(
br_val.get(field).is_some(),
"br JSONL missing required field: {}",
field
);
assert!(
bd_val.get(field).is_some(),
"bd JSONL missing required field: {}",
field
);
}
info!("conformance_sync_flush_jsonl_line_format passed");
}
#[test]
fn conformance_sync_flush_with_comments() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_flush_with_comments test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_issue = workspace.run_br(["create", "Commented issue", "--json"], "create");
let bd_issue = workspace.run_bd(["create", "Commented issue", "--json"], "create");
let br_id = extract_issue_id(&extract_json_payload(&br_issue.stdout));
let bd_id = extract_issue_id(&extract_json_payload(&bd_issue.stdout));
workspace.run_br(["comments", "add", &br_id, "Test comment"], "add_comment");
workspace.run_bd(["comments", "add", &bd_id, "Test comment"], "add_comment");
let br_sync = workspace.run_br(["sync", "--flush-only"], "flush");
let bd_sync = workspace.run_bd(["sync", "--flush-only"], "flush");
assert!(br_sync.status.success(), "br flush failed");
assert!(bd_sync.status.success(), "bd flush failed");
let br_jsonl = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_jsonl = workspace.bd_root.join(".beads").join("issues.jsonl");
let br_content = fs::read_to_string(&br_jsonl).expect("read br jsonl");
let bd_content = fs::read_to_string(&bd_jsonl).expect("read bd jsonl");
assert!(!br_content.trim().is_empty(), "br JSONL is empty");
assert!(!bd_content.trim().is_empty(), "bd JSONL is empty");
info!("conformance_sync_flush_with_comments passed");
}
#[test]
fn conformance_sync_import_empty_jsonl() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_import_empty_jsonl test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_jsonl = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_jsonl = workspace.bd_root.join(".beads").join("issues.jsonl");
fs::write(&br_jsonl, "").expect("write br jsonl");
fs::write(&bd_jsonl, "").expect("write bd jsonl");
let br_import = workspace.run_br(["sync", "--import-only"], "import_empty");
let bd_import = workspace.run_bd(["sync", "--import-only"], "import_empty");
assert_eq!(
br_import.status.success(),
bd_import.status.success(),
"import empty behavior differs: br={}, bd={}",
br_import.status.success(),
bd_import.status.success()
);
let br_list = workspace.run_br(["list", "--json"], "list");
let bd_list = workspace.run_bd(["list", "--json"], "list");
let br_json = extract_json_payload(&br_list.stdout);
let bd_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_json).unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"import empty counts differ: br={}, bd={}",
br_len, bd_len
);
info!("conformance_sync_import_empty_jsonl passed");
}
#[test]
fn conformance_sync_import_single_issue() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_import_single_issue test");
let source_workspace = ConformanceWorkspace::new();
source_workspace.init_both();
source_workspace.run_br(["create", "Single import test"], "create");
source_workspace.run_bd(["create", "Single import test"], "create");
source_workspace.run_br(["sync", "--flush-only"], "export");
source_workspace.run_bd(["sync", "--flush-only"], "export");
let import_workspace = ConformanceWorkspace::new();
import_workspace.init_both();
let br_src = source_workspace.br_root.join(".beads").join("issues.jsonl");
let bd_src = source_workspace.bd_root.join(".beads").join("issues.jsonl");
let br_dst = import_workspace.br_root.join(".beads").join("issues.jsonl");
let bd_dst = import_workspace.bd_root.join(".beads").join("issues.jsonl");
fs::copy(&br_src, &br_dst).expect("copy br jsonl");
fs::copy(&bd_src, &bd_dst).expect("copy bd jsonl");
let br_import = import_workspace.run_br(["sync", "--import-only"], "import");
let bd_import = import_workspace.run_bd(["sync", "--import-only"], "import");
assert!(br_import.status.success(), "br import failed");
assert!(bd_import.status.success(), "bd import failed");
let br_list = import_workspace.run_br(["list", "--json"], "list");
let bd_list = import_workspace.run_bd(["list", "--json"], "list");
let br_val: Value = serde_json::from_str(&extract_json_payload(&br_list.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&extract_json_payload(&bd_list.stdout))
.unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(br_len, bd_len, "single import counts differ");
assert_eq!(br_len, 1, "expected 1 issue after single import");
info!("conformance_sync_import_single_issue passed");
}
#[test]
fn conformance_sync_import_many_issues() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_import_many_issues test");
let source_workspace = ConformanceWorkspace::new();
source_workspace.init_both();
for i in 0..10 {
source_workspace.run_br(
["create", &format!("Many import {}", i)],
&format!("create_{}", i),
);
source_workspace.run_bd(
["create", &format!("Many import {}", i)],
&format!("create_{}", i),
);
}
source_workspace.run_br(["sync", "--flush-only"], "export");
source_workspace.run_bd(["sync", "--flush-only"], "export");
let import_workspace = ConformanceWorkspace::new();
import_workspace.init_both();
let br_src = source_workspace.br_root.join(".beads").join("issues.jsonl");
let bd_src = source_workspace.bd_root.join(".beads").join("issues.jsonl");
let br_dst = import_workspace.br_root.join(".beads").join("issues.jsonl");
let bd_dst = import_workspace.bd_root.join(".beads").join("issues.jsonl");
fs::copy(&br_src, &br_dst).expect("copy br jsonl");
fs::copy(&bd_src, &bd_dst).expect("copy bd jsonl");
let br_import = import_workspace.run_br(["sync", "--import-only"], "import");
let bd_import = import_workspace.run_bd(["sync", "--import-only"], "import");
assert!(br_import.status.success(), "br import failed");
assert!(bd_import.status.success(), "bd import failed");
let br_list = import_workspace.run_br(["list", "--json"], "list");
let bd_list = import_workspace.run_bd(["list", "--json"], "list");
let br_val: Value = serde_json::from_str(&extract_json_payload(&br_list.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&extract_json_payload(&bd_list.stdout))
.unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"many import counts differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 10, "expected 10 issues after many import");
info!("conformance_sync_import_many_issues passed");
}
#[test]
fn conformance_sync_import_updates_existing() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_import_updates_existing test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_issue = workspace.run_br(["create", "Update test issue", "--json"], "create");
let bd_issue = workspace.run_bd(["create", "Update test issue", "--json"], "create");
let br_id = extract_issue_id(&extract_json_payload(&br_issue.stdout));
let bd_id = extract_issue_id(&extract_json_payload(&bd_issue.stdout));
workspace.run_br(["sync", "--flush-only"], "export1");
workspace.run_bd(["sync", "--flush-only"], "export1");
workspace.run_br(["update", &br_id, "--priority", "1"], "update");
workspace.run_bd(["update", &bd_id, "--priority", "1"], "update");
workspace.run_br(["sync", "--flush-only"], "export2");
workspace.run_bd(["sync", "--flush-only"], "export2");
let br_import = workspace.run_br(["sync", "--import-only"], "import");
let bd_import = workspace.run_bd(["sync", "--import-only"], "import");
assert!(br_import.status.success(), "br import failed");
assert!(bd_import.status.success(), "bd import failed");
let br_list = workspace.run_br(["list", "--json"], "list");
let bd_list = workspace.run_bd(["list", "--json"], "list");
let br_val: Value = serde_json::from_str(&extract_json_payload(&br_list.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&extract_json_payload(&bd_list.stdout))
.unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(br_len, bd_len, "update existing counts differ");
assert_eq!(br_len, 1, "expected 1 issue (not duplicated)");
info!("conformance_sync_import_updates_existing passed");
}
#[test]
fn conformance_sync_roundtrip_preserves_all_fields() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_roundtrip_preserves_all_fields test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(
[
"create",
"Full field test",
"--type",
"feature",
"--priority",
"2",
"--description",
"Test description",
],
"create",
);
workspace.run_bd(
[
"create",
"Full field test",
"--type",
"feature",
"--priority",
"2",
"--description",
"Test description",
],
"create",
);
workspace.run_br(["sync", "--flush-only"], "export");
workspace.run_bd(["sync", "--flush-only"], "export");
let import_workspace = ConformanceWorkspace::new();
import_workspace.init_both();
let br_src = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_src = workspace.bd_root.join(".beads").join("issues.jsonl");
let br_dst = import_workspace.br_root.join(".beads").join("issues.jsonl");
let bd_dst = import_workspace.bd_root.join(".beads").join("issues.jsonl");
fs::copy(&br_src, &br_dst).expect("copy br jsonl");
fs::copy(&bd_src, &bd_dst).expect("copy bd jsonl");
import_workspace.run_br(["sync", "--import-only"], "import");
import_workspace.run_bd(["sync", "--import-only"], "import");
let br_list = import_workspace.run_br(["list", "--json"], "list");
let bd_list = import_workspace.run_bd(["list", "--json"], "list");
let br_val: Value =
serde_json::from_str(&extract_json_payload(&br_list.stdout)).expect("parse br");
let bd_val: Value =
serde_json::from_str(&extract_json_payload(&bd_list.stdout)).expect("parse bd");
let br_issue = &br_val[0];
let bd_issue = &bd_val[0];
assert_eq!(br_issue["title"], bd_issue["title"], "titles should match");
assert_eq!(
br_issue["priority"], bd_issue["priority"],
"priorities should match"
);
info!("conformance_sync_roundtrip_preserves_all_fields passed");
}
#[test]
fn conformance_sync_roundtrip_unicode() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_roundtrip_unicode test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let unicode_title = "Unicode: 你好世界 🎉 café";
workspace.run_br(["create", unicode_title], "create");
workspace.run_bd(["create", unicode_title], "create");
workspace.run_br(["sync", "--flush-only"], "export");
workspace.run_bd(["sync", "--flush-only"], "export");
let import_workspace = ConformanceWorkspace::new();
import_workspace.init_both();
let br_src = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_src = workspace.bd_root.join(".beads").join("issues.jsonl");
let br_dst = import_workspace.br_root.join(".beads").join("issues.jsonl");
let bd_dst = import_workspace.bd_root.join(".beads").join("issues.jsonl");
fs::copy(&br_src, &br_dst).expect("copy br jsonl");
fs::copy(&bd_src, &bd_dst).expect("copy bd jsonl");
import_workspace.run_br(["sync", "--import-only"], "import");
import_workspace.run_bd(["sync", "--import-only"], "import");
let br_list = import_workspace.run_br(["list", "--json"], "list");
let bd_list = import_workspace.run_bd(["list", "--json"], "list");
let br_val: Value =
serde_json::from_str(&extract_json_payload(&br_list.stdout)).expect("parse br");
let bd_val: Value =
serde_json::from_str(&extract_json_payload(&bd_list.stdout)).expect("parse bd");
let br_title = br_val[0]["title"].as_str().unwrap_or("");
let bd_title = bd_val[0]["title"].as_str().unwrap_or("");
assert!(br_title.contains("你好"), "br should preserve Chinese");
assert!(bd_title.contains("你好"), "bd should preserve Chinese");
assert!(br_title.contains("🎉"), "br should preserve emoji");
assert!(bd_title.contains("🎉"), "bd should preserve emoji");
info!("conformance_sync_roundtrip_unicode passed");
}
#[test]
fn conformance_sync_roundtrip_special_chars() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_roundtrip_special_chars test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let special_title = r#"Special: "quotes" and \backslash and 'apostrophe'"#;
workspace.run_br(["create", special_title], "create");
workspace.run_bd(["create", special_title], "create");
workspace.run_br(["sync", "--flush-only"], "export");
workspace.run_bd(["sync", "--flush-only"], "export");
let br_jsonl = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_jsonl = workspace.bd_root.join(".beads").join("issues.jsonl");
let br_content = fs::read_to_string(&br_jsonl).expect("read br jsonl");
let bd_content = fs::read_to_string(&bd_jsonl).expect("read bd jsonl");
let br_val: Value = serde_json::from_str(br_content.lines().next().unwrap())
.expect("br JSONL should be valid JSON with special chars");
let bd_val: Value = serde_json::from_str(bd_content.lines().next().unwrap())
.expect("bd JSONL should be valid JSON with special chars");
let br_title = br_val["title"].as_str().unwrap_or("");
let bd_title = bd_val["title"].as_str().unwrap_or("");
assert!(br_title.contains("quotes"), "br should preserve quotes");
assert!(bd_title.contains("quotes"), "bd should preserve quotes");
info!("conformance_sync_roundtrip_special_chars passed");
}
#[test]
fn conformance_sync_status_clean() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_status_clean test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Status test"], "create");
workspace.run_br(["sync", "--flush-only"], "flush");
let br_status = workspace.run_br(["sync", "--status"], "status");
assert!(br_status.status.success(), "br status failed");
info!("br status: {}", br_status.stdout);
info!("conformance_sync_status_clean passed");
}
#[test]
fn conformance_sync_status_json_output() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_status_json_output test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "JSON status test"], "create");
workspace.run_br(["sync", "--flush-only"], "flush");
let br_status = workspace.run_br(["sync", "--status", "--json"], "status_json");
assert!(br_status.status.success(), "br status --json failed");
let br_json = extract_json_payload(&br_status.stdout);
let _br_val: Value =
serde_json::from_str(&br_json).expect("br status --json should produce valid JSON");
info!("conformance_sync_status_json_output passed");
}
#[test]
fn conformance_sync_large_description() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_large_description test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let large_desc: String = "x".repeat(10_000);
workspace.run_br(
["create", "Large desc test", "--description", &large_desc],
"create",
);
workspace.run_bd(
["create", "Large desc test", "--description", &large_desc],
"create",
);
let br_sync = workspace.run_br(["sync", "--flush-only"], "flush");
let bd_sync = workspace.run_bd(["sync", "--flush-only"], "flush");
assert!(br_sync.status.success(), "br flush large desc failed");
assert!(bd_sync.status.success(), "bd flush large desc failed");
let br_jsonl = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_jsonl = workspace.bd_root.join(".beads").join("issues.jsonl");
let br_content = fs::read_to_string(&br_jsonl).expect("read br jsonl");
let bd_content = fs::read_to_string(&bd_jsonl).expect("read bd jsonl");
let br_val: Value = serde_json::from_str(br_content.lines().next().unwrap())
.expect("br large desc should be valid JSON");
let bd_val: Value = serde_json::from_str(bd_content.lines().next().unwrap())
.expect("bd large desc should be valid JSON");
let br_desc = br_val["description"].as_str().unwrap_or("");
let bd_desc = bd_val["description"].as_str().unwrap_or("");
assert!(
br_desc.len() >= 9000,
"br should preserve large description"
);
assert!(
bd_desc.len() >= 9000,
"bd should preserve large description"
);
info!("conformance_sync_large_description passed");
}
#[test]
fn conformance_sync_tombstones() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_tombstones test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_issue = workspace.run_br(["create", "Tombstone test", "--json"], "create");
let bd_issue = workspace.run_bd(["create", "Tombstone test", "--json"], "create");
let br_id = extract_issue_id(&extract_json_payload(&br_issue.stdout));
let bd_id = extract_issue_id(&extract_json_payload(&bd_issue.stdout));
workspace.run_br(["delete", &br_id], "delete");
workspace.run_bd(["delete", &bd_id], "delete");
let br_sync = workspace.run_br(["sync", "--flush-only"], "flush");
let bd_sync = workspace.run_bd(["sync", "--flush-only"], "flush");
info!(
"Tombstone export: br={}, bd={}",
br_sync.status.success(),
bd_sync.status.success()
);
info!("conformance_sync_tombstones passed");
}
#[test]
fn conformance_init_reinit() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_init_reinit test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_reinit = workspace.run_br(["init"], "reinit");
let bd_reinit = workspace.run_bd(["init"], "reinit");
assert_eq!(
br_reinit.status.success(),
bd_reinit.status.success(),
"reinit behavior differs: br success={}, bd success={}",
br_reinit.status.success(),
bd_reinit.status.success()
);
assert!(
workspace.br_root.join(".beads").exists(),
"br .beads disappeared after reinit"
);
assert!(
workspace.bd_root.join(".beads").exists(),
"bd .beads disappeared after reinit"
);
info!("conformance_init_reinit passed");
}
#[test]
fn conformance_init_existing_db() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_init_existing_db test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Test issue"], "create");
workspace.run_bd(["create", "Test issue"], "create");
workspace.run_br(["init"], "init_again");
workspace.run_bd(["init"], "init_again");
let br_list = workspace.run_br(["list", "--json"], "list_after");
let bd_list = workspace.run_bd(["list", "--json"], "list_after");
let br_json = extract_json_payload(&br_list.stdout);
let bd_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_json).unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(br_len, bd_len, "issue counts differ after reinit");
info!("conformance_init_existing_db passed");
}
#[test]
fn conformance_init_creates_beads_dir() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_init_creates_beads_dir test");
let workspace = ConformanceWorkspace::new();
assert!(!workspace.br_root.join(".beads").exists());
assert!(!workspace.bd_root.join(".beads").exists());
workspace.init_both();
assert!(
workspace.br_root.join(".beads").join("beads.db").exists(),
"br did not create .beads/beads.db"
);
assert!(
workspace.bd_root.join(".beads").join("issues.db").exists()
|| workspace.bd_root.join(".beads").join("beads.db").exists(),
"bd did not create a database file"
);
info!("conformance_init_creates_beads_dir passed");
}
#[test]
fn conformance_init_json_output() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_init_json_output test");
let workspace = ConformanceWorkspace::new();
let br_init = workspace.run_br(["init", "--json"], "init_json");
let bd_init = workspace.run_bd(["init", "--json"], "init_json");
assert!(
br_init.status.success(),
"br init --json failed: {}",
br_init.stderr
);
assert!(
bd_init.status.success(),
"bd init --json failed: {}",
bd_init.stderr
);
let br_json = extract_json_payload(&br_init.stdout);
let bd_json = extract_json_payload(&bd_init.stdout);
if !br_json.is_empty() && !bd_json.is_empty() {
let br_val: Result<Value, _> = serde_json::from_str(&br_json);
let bd_val: Result<Value, _> = serde_json::from_str(&bd_json);
assert_eq!(
br_val.is_ok(),
bd_val.is_ok(),
"JSON validity differs: br valid={}, bd valid={}",
br_val.is_ok(),
bd_val.is_ok()
);
}
info!("conformance_init_json_output passed");
}
#[test]
fn conformance_init_config() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_init_config test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_config_path = workspace.br_root.join(".beads").join("config.yaml");
let bd_config_path = workspace.bd_root.join(".beads").join("config.yaml");
assert!(
br_config_path.exists(),
"br config.yaml missing at {}",
br_config_path.display()
);
assert!(
bd_config_path.exists(),
"bd config.yaml missing at {}",
bd_config_path.display()
);
let br_config = fs::read_to_string(&br_config_path).expect("read br config.yaml");
let bd_config = fs::read_to_string(&bd_config_path).expect("read bd config.yaml");
assert!(!br_config.trim().is_empty(), "br config.yaml is empty");
assert!(!bd_config.trim().is_empty(), "bd config.yaml is empty");
let br_yaml: Result<YamlValue, _> = serde_yml::from_str(&br_config);
let bd_yaml: Result<YamlValue, _> = serde_yml::from_str(&bd_config);
assert_eq!(
br_yaml.is_ok(),
bd_yaml.is_ok(),
"config YAML validity differs: br ok={}, bd ok={}",
br_yaml.is_ok(),
bd_yaml.is_ok()
);
if let (Ok(br_val), Ok(bd_val)) = (br_yaml, bd_yaml) {
assert_eq!(br_val, bd_val, "config YAML content differs after parsing");
}
info!("conformance_init_config passed");
}
#[test]
fn conformance_init_metadata() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_init_metadata test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_metadata_path = workspace.br_root.join(".beads").join("metadata.json");
let bd_metadata_path = workspace.bd_root.join(".beads").join("metadata.json");
assert!(
br_metadata_path.exists(),
"br metadata.json missing at {}",
br_metadata_path.display()
);
assert!(
bd_metadata_path.exists(),
"bd metadata.json missing at {}",
bd_metadata_path.display()
);
let br_metadata = fs::read_to_string(&br_metadata_path).expect("read br metadata.json");
let bd_metadata = fs::read_to_string(&bd_metadata_path).expect("read bd metadata.json");
let result = compare_json(&br_metadata, &bd_metadata, &CompareMode::ExactJson);
assert!(result.is_ok(), "metadata JSON mismatch: {:?}", result.err());
info!("conformance_init_metadata passed");
}
#[test]
fn conformance_create_all_types() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_create_all_types test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let types = ["bug", "feature", "task", "epic", "chore"];
for issue_type in types {
let title = format!("Test {} issue", issue_type);
let br_create = workspace.run_br(
["create", &title, "--type", issue_type, "--json"],
&format!("create_{}", issue_type),
);
let bd_create = workspace.run_bd(
["create", &title, "--type", issue_type, "--json"],
&format!("create_{}", issue_type),
);
assert!(
br_create.status.success(),
"br create --type {} failed: {}",
issue_type,
br_create.stderr
);
assert!(
bd_create.status.success(),
"bd create --type {} failed: {}",
issue_type,
bd_create.stderr
);
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let result = compare_json(
&br_json,
&bd_json,
&CompareMode::ContainsFields(vec!["issue_type".to_string()]),
);
assert!(
result.is_ok(),
"type {} comparison failed: {:?}",
issue_type,
result.err()
);
}
info!("conformance_create_all_types passed");
}
#[test]
fn conformance_create_all_priorities() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_create_all_priorities test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
for priority in 0..=4 {
let title = format!("Priority {} issue", priority);
let priority_str = priority.to_string();
let br_create = workspace.run_br(
["create", &title, "--priority", &priority_str, "--json"],
&format!("create_p{}", priority),
);
let bd_create = workspace.run_bd(
["create", &title, "--priority", &priority_str, "--json"],
&format!("create_p{}", priority),
);
assert!(
br_create.status.success(),
"br create --priority {} failed: {}",
priority,
br_create.stderr
);
assert!(
bd_create.status.success(),
"bd create --priority {} failed: {}",
priority,
bd_create.stderr
);
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse br");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse bd");
let br_p = br_val["priority"]
.as_i64()
.or_else(|| br_val[0]["priority"].as_i64());
let bd_p = bd_val["priority"]
.as_i64()
.or_else(|| bd_val[0]["priority"].as_i64());
assert_eq!(
br_p, bd_p,
"priority {} mismatch: br={:?}, bd={:?}",
priority, br_p, bd_p
);
}
info!("conformance_create_all_priorities passed");
}
#[test]
fn conformance_create_with_assignee() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_create_with_assignee test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(
["create", "Assigned issue", "--assignee", "alice", "--json"],
"create_assigned",
);
let bd_create = workspace.run_bd(
["create", "Assigned issue", "--assignee", "alice", "--json"],
"create_assigned",
);
assert!(
br_create.status.success(),
"br create failed: {}",
br_create.stderr
);
assert!(
bd_create.status.success(),
"bd create failed: {}",
bd_create.stderr
);
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_assignee = br_val["assignee"]
.as_str()
.or_else(|| br_val[0]["assignee"].as_str());
let bd_assignee = bd_val["assignee"]
.as_str()
.or_else(|| bd_val[0]["assignee"].as_str());
assert_eq!(
br_assignee, bd_assignee,
"assignee mismatch: br={:?}, bd={:?}",
br_assignee, bd_assignee
);
info!("conformance_create_with_assignee passed");
}
#[test]
fn conformance_create_with_description() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_create_with_description test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let desc = "This is a detailed description\nwith multiple lines.";
let br_create = workspace.run_br(
["create", "Issue with desc", "--description", desc, "--json"],
"create_desc",
);
let bd_create = workspace.run_bd(
["create", "Issue with desc", "--description", desc, "--json"],
"create_desc",
);
assert!(
br_create.status.success(),
"br create failed: {}",
br_create.stderr
);
assert!(
bd_create.status.success(),
"bd create failed: {}",
bd_create.stderr
);
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_desc = br_val["description"]
.as_str()
.or_else(|| br_val[0]["description"].as_str());
let bd_desc = bd_val["description"]
.as_str()
.or_else(|| bd_val[0]["description"].as_str());
assert_eq!(
br_desc, bd_desc,
"description mismatch: br={:?}, bd={:?}",
br_desc, bd_desc
);
info!("conformance_create_with_description passed");
}
#[test]
fn conformance_create_unicode_title() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_create_unicode_title test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let unicode_titles = [
"日本語のタイトル", "Emoji test 🎉🚀💻", "مرحبا بالعالم", "Ñoño español", "Über Größe", ];
for title in unicode_titles {
let br_create = workspace.run_br(["create", title, "--json"], "create_unicode");
let bd_create = workspace.run_bd(["create", title, "--json"], "create_unicode");
assert!(
br_create.status.success(),
"br create unicode failed for '{}': {}",
title,
br_create.stderr
);
assert!(
bd_create.status.success(),
"bd create unicode failed for '{}': {}",
title,
bd_create.stderr
);
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_title = br_val["title"]
.as_str()
.or_else(|| br_val[0]["title"].as_str());
let bd_title = bd_val["title"]
.as_str()
.or_else(|| bd_val[0]["title"].as_str());
assert_eq!(
br_title, bd_title,
"unicode title mismatch for '{}': br={:?}, bd={:?}",
title, br_title, bd_title
);
}
info!("conformance_create_unicode_title passed");
}
#[test]
fn conformance_create_special_chars() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_create_special_chars test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let special_titles = [
"Title with 'single quotes'",
"Title with \"double quotes\"",
"Title with \\backslashes\\",
"Title with <angle> & ampersand",
];
for title in special_titles {
let br_create = workspace.run_br(["create", title, "--json"], "create_special");
let bd_create = workspace.run_bd(["create", title, "--json"], "create_special");
assert!(
br_create.status.success(),
"br create special failed for '{}': {}",
title,
br_create.stderr
);
assert!(
bd_create.status.success(),
"bd create special failed for '{}': {}",
title,
bd_create.stderr
);
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_title = br_val["title"]
.as_str()
.or_else(|| br_val[0]["title"].as_str());
let bd_title = bd_val["title"]
.as_str()
.or_else(|| bd_val[0]["title"].as_str());
assert_eq!(
br_title, bd_title,
"special char title mismatch for '{}': br={:?}, bd={:?}",
title, br_title, bd_title
);
}
info!("conformance_create_special_chars passed");
}
#[test]
fn conformance_create_very_long_title() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_create_very_long_title test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let long_title = "A".repeat(500);
let br_create = workspace.run_br(["create", &long_title, "--json"], "create_long");
let bd_create = workspace.run_bd(["create", &long_title, "--json"], "create_long");
assert!(
br_create.status.success(),
"br create long title failed: {}",
br_create.stderr
);
assert!(
bd_create.status.success(),
"bd create long title failed: {}",
bd_create.stderr
);
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_title = br_val["title"]
.as_str()
.or_else(|| br_val[0]["title"].as_str());
let bd_title = bd_val["title"]
.as_str()
.or_else(|| bd_val[0]["title"].as_str());
assert_eq!(
br_title,
bd_title,
"long title mismatch: br_len={:?}, bd_len={:?}",
br_title.map(str::len),
bd_title.map(str::len)
);
assert_eq!(br_title.map(str::len), Some(500), "expected 500-char title");
info!("conformance_create_very_long_title passed");
}
#[test]
fn conformance_create_empty_title_error() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_create_empty_title_error test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "", "--json"], "create_empty");
let bd_create = workspace.run_bd(["create", "", "--json"], "create_empty");
assert_eq!(
br_create.status.success(),
bd_create.status.success(),
"empty title behavior differs: br success={}, bd success={}",
br_create.status.success(),
bd_create.status.success()
);
assert!(
!br_create.status.success(),
"expected empty title to fail in br"
);
info!("conformance_create_empty_title_error passed");
}
#[test]
fn conformance_create_with_external_ref() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_create_with_external_ref test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(
[
"create",
"Issue with external ref",
"--external-ref",
"JIRA-123",
"--json",
],
"create_external_ref",
);
let bd_create = workspace.run_bd(
[
"create",
"Issue with external ref",
"--external-ref",
"JIRA-123",
"--json",
],
"create_external_ref",
);
assert!(
br_create.status.success(),
"br create failed: {}",
br_create.stderr
);
assert!(
bd_create.status.success(),
"bd create failed: {}",
bd_create.stderr
);
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_ref = br_val["external_ref"]
.as_str()
.or_else(|| br_val[0]["external_ref"].as_str());
let bd_ref = bd_val["external_ref"]
.as_str()
.or_else(|| bd_val[0]["external_ref"].as_str());
assert_eq!(
br_ref, bd_ref,
"external_ref mismatch: br={:?}, bd={:?}",
br_ref, bd_ref
);
info!("conformance_create_with_external_ref passed");
}
#[test]
fn conformance_create_invalid_priority_error() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_create_invalid_priority_error test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(
["create", "Bad priority issue", "--priority", "9", "--json"],
"create_bad_priority",
);
let bd_create = workspace.run_bd(
["create", "Bad priority issue", "--priority", "9", "--json"],
"create_bad_priority",
);
assert_eq!(
br_create.status.success(),
bd_create.status.success(),
"invalid priority behavior differs: br success={}, bd success={}",
br_create.status.success(),
bd_create.status.success()
);
assert!(
!br_create.status.success(),
"expected invalid priority to fail in br"
);
info!("conformance_create_invalid_priority_error passed");
}
#[test]
#[ignore = "bd v0.46.0 list --status filter returns invalid JSON: known behavioral difference"]
fn conformance_list_filter_status_closed() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_list_filter_status_closed test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Open issue", "--json"], "create_open");
let bd_create = workspace.run_br_in_bd_env(["create", "Open issue", "--json"], "create_open");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
workspace.run_br(["close", br_id], "close_one");
workspace.run_br_in_bd_env(["close", bd_id], "close_one");
let br_list = workspace.run_br(["list", "--status", "closed", "--json"], "list_closed");
let bd_list = workspace.run_bd(["list", "--status", "closed", "--json"], "list_closed");
assert!(
br_list.status.success(),
"br list closed failed: {}",
br_list.stderr
);
assert!(
bd_list.status.success(),
"bd list closed failed: {}",
bd_list.stderr
);
let br_list_json = extract_json_payload(&br_list.stdout);
let bd_list_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_list_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_list_json).unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"closed list lengths differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 1, "expected 1 closed issue");
info!("conformance_list_filter_status_closed passed");
}
#[test]
#[ignore = "bd v0.46.0 list --assignee filter returns empty: known behavioral difference"]
fn conformance_list_filter_assignee() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_list_filter_assignee test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(
["create", "Assigned to alice", "--assignee", "alice"],
"create_alice",
);
workspace.run_br_in_bd_env(
["create", "Assigned to alice", "--assignee", "alice"],
"create_alice",
);
workspace.run_br(
["create", "Assigned to bob", "--assignee", "bob"],
"create_bob",
);
workspace.run_br_in_bd_env(
["create", "Assigned to bob", "--assignee", "bob"],
"create_bob",
);
let br_list = workspace.run_br(
["list", "--assignee", "alice", "--json"],
"list_assignee_alice",
);
let bd_list = workspace.run_bd(
["list", "--assignee", "alice", "--json"],
"list_assignee_alice",
);
assert!(
br_list.status.success(),
"br list assignee failed: {}",
br_list.stderr
);
assert!(
bd_list.status.success(),
"bd list assignee failed: {}",
bd_list.stderr
);
let br_list_json = extract_json_payload(&br_list.stdout);
let bd_list_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_list_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_list_json).unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"assignee list lengths differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 1, "expected 1 issue assigned to alice");
info!("conformance_list_filter_assignee passed");
}
#[test]
#[ignore = "bd v0.46.0 list --limit returns empty: known behavioral difference"]
fn conformance_list_limit() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_list_limit test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Issue 1"], "create1");
workspace.run_br_in_bd_env(["create", "Issue 1"], "create1");
workspace.run_br(["create", "Issue 2"], "create2");
workspace.run_br_in_bd_env(["create", "Issue 2"], "create2");
workspace.run_br(["create", "Issue 3"], "create3");
workspace.run_br_in_bd_env(["create", "Issue 3"], "create3");
let br_list = workspace.run_br(["list", "--limit", "1", "--json"], "list_limit");
let bd_list = workspace.run_bd(["list", "--limit", "1", "--json"], "list_limit");
assert!(
br_list.status.success(),
"br list limit failed: {}",
br_list.stderr
);
assert!(
bd_list.status.success(),
"bd list limit failed: {}",
bd_list.stderr
);
let br_list_json = extract_json_payload(&br_list.stdout);
let bd_list_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_list_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_list_json).unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"limit list lengths differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 1, "expected 1 issue with limit");
info!("conformance_list_limit passed");
}
#[test]
#[ignore = "bd v0.46.0 list --status filter returns invalid JSON: known behavioral difference"]
fn conformance_list_filter_status_open() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_list_filter_status_open test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Open issue", "--json"], "create_open");
workspace.run_br_in_bd_env(["create", "Open issue", "--json"], "create_open");
let br_create_closed = workspace.run_br(["create", "Closed issue", "--json"], "create_closed");
let bd_create_closed =
workspace.run_br_in_bd_env(["create", "Closed issue", "--json"], "create_closed");
let br_closed_json = extract_json_payload(&br_create_closed.stdout);
let bd_closed_json = extract_json_payload(&bd_create_closed.stdout);
let br_closed_val: Value = serde_json::from_str(&br_closed_json).expect("parse");
let bd_closed_val: Value = serde_json::from_str(&bd_closed_json).expect("parse");
let br_closed_id = br_closed_val["id"]
.as_str()
.or_else(|| br_closed_val[0]["id"].as_str())
.unwrap();
let bd_closed_id = bd_closed_val["id"]
.as_str()
.or_else(|| bd_closed_val[0]["id"].as_str())
.unwrap();
workspace.run_br(["close", br_closed_id], "close_closed");
workspace.run_bd(["close", bd_closed_id], "close_closed");
let br_list = workspace.run_br(["list", "--status", "open", "--json"], "list_open");
let bd_list = workspace.run_bd(["list", "--status", "open", "--json"], "list_open");
assert!(
br_list.status.success(),
"br list open failed: {}",
br_list.stderr
);
assert!(
bd_list.status.success(),
"bd list open failed: {}",
bd_list.stderr
);
let br_list_json = extract_json_payload(&br_list.stdout);
let bd_list_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_list_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_list_json).unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"open list lengths differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 1, "expected 1 open issue");
info!("conformance_list_filter_status_open passed");
}
#[test]
#[ignore = "bd v0.46.0 list --status filter returns invalid JSON: known behavioral difference"]
fn conformance_list_filter_status_in_progress() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_list_filter_status_in_progress test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "In progress issue", "--json"], "create_ip");
let bd_create =
workspace.run_br_in_bd_env(["create", "In progress issue", "--json"], "create_ip");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
workspace.run_br(["update", br_id, "--status", "in_progress"], "update_ip");
workspace.run_br_in_bd_env(["update", bd_id, "--status", "in_progress"], "update_ip");
let br_list = workspace.run_br(
["list", "--status", "in_progress", "--json"],
"list_in_progress",
);
let bd_list = workspace.run_bd(
["list", "--status", "in_progress", "--json"],
"list_in_progress",
);
assert!(
br_list.status.success(),
"br list in_progress failed: {}",
br_list.stderr
);
assert!(
bd_list.status.success(),
"bd list in_progress failed: {}",
bd_list.stderr
);
let br_list_json = extract_json_payload(&br_list.stdout);
let bd_list_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_list_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_list_json).unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"in_progress list lengths differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 1, "expected 1 in_progress issue");
info!("conformance_list_filter_status_in_progress passed");
}
#[test]
#[ignore = "bd v0.46.0 list --priority range returns empty: known behavioral difference"]
fn conformance_list_filter_priority_range() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_list_filter_priority_range test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "P0 issue", "--priority", "0"], "create_p0");
workspace.run_br_in_bd_env(["create", "P0 issue", "--priority", "0"], "create_p0");
workspace.run_br(["create", "P1 issue", "--priority", "1"], "create_p1");
workspace.run_br_in_bd_env(["create", "P1 issue", "--priority", "1"], "create_p1");
workspace.run_br(["create", "P3 issue", "--priority", "3"], "create_p3");
workspace.run_br_in_bd_env(["create", "P3 issue", "--priority", "3"], "create_p3");
let br_list = workspace.run_br(
[
"list",
"--priority-min",
"0",
"--priority-max",
"1",
"--json",
],
"list_priority_range",
);
let bd_list = workspace.run_bd(
[
"list",
"--priority-min",
"0",
"--priority-max",
"1",
"--json",
],
"list_priority_range",
);
assert!(
br_list.status.success(),
"br list priority range failed: {}",
br_list.stderr
);
assert!(
bd_list.status.success(),
"bd list priority range failed: {}",
bd_list.stderr
);
let br_list_json = extract_json_payload(&br_list.stdout);
let bd_list_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_list_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_list_json).unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"priority range lengths differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 2, "expected 2 issues in priority range");
info!("conformance_list_filter_priority_range passed");
}
#[test]
#[ignore = "bd v0.46.0 list --label filter returns invalid JSON: known behavioral difference"]
fn conformance_list_filter_label() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_list_filter_label test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Label issue", "--json"], "create_label");
let bd_create = workspace.run_br_in_bd_env(["create", "Label issue", "--json"], "create_label");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
workspace.run_br(["label", "add", br_id, "urgent"], "label_add");
workspace.run_br_in_bd_env(["label", "add", bd_id, "urgent"], "label_add");
workspace.run_br(["create", "Unlabeled issue"], "create_unlabeled");
workspace.run_br_in_bd_env(["create", "Unlabeled issue"], "create_unlabeled");
let br_list = workspace.run_br(["list", "--label", "urgent", "--json"], "list_label");
let bd_list = workspace.run_bd(["list", "--label", "urgent", "--json"], "list_label");
assert!(
br_list.status.success(),
"br list label failed: {}",
br_list.stderr
);
assert!(
bd_list.status.success(),
"bd list label failed: {}",
bd_list.stderr
);
let br_list_json = extract_json_payload(&br_list.stdout);
let bd_list_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_list_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_list_json).unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"label list lengths differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 1, "expected 1 labeled issue");
info!("conformance_list_filter_label passed");
}
#[test]
#[ignore = "bd v0.46.0 list with multiple filters returns invalid JSON: known behavioral difference"]
fn conformance_list_filter_multiple() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_list_filter_multiple test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(
[
"create",
"Multi filter issue",
"--assignee",
"alice",
"--json",
],
"create_multi",
);
let bd_create = workspace.run_br_in_bd_env(
[
"create",
"Multi filter issue",
"--assignee",
"alice",
"--json",
],
"create_multi",
);
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
workspace.run_br(["label", "add", br_id, "urgent"], "label_add");
workspace.run_br_in_bd_env(["label", "add", bd_id, "urgent"], "label_add");
workspace.run_br(
["create", "Other issue", "--assignee", "alice"],
"create_other",
);
workspace.run_br_in_bd_env(
["create", "Other issue", "--assignee", "alice"],
"create_other",
);
let br_list = workspace.run_br(
["list", "--assignee", "alice", "--label", "urgent", "--json"],
"list_multi",
);
let bd_list = workspace.run_bd(
["list", "--assignee", "alice", "--label", "urgent", "--json"],
"list_multi",
);
assert!(
br_list.status.success(),
"br list multi failed: {}",
br_list.stderr
);
assert!(
bd_list.status.success(),
"bd list multi failed: {}",
bd_list.stderr
);
let br_list_json = extract_json_payload(&br_list.stdout);
let bd_list_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_list_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_list_json).unwrap_or(Value::Array(vec![]));
let br_len = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"multi-filter list lengths differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 1, "expected 1 issue matching both filters");
info!("conformance_list_filter_multiple passed");
}
#[test]
fn conformance_list_sort_priority() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_list_sort_priority test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "P2 issue", "--priority", "2"], "create_p2");
workspace.run_bd(["create", "P2 issue", "--priority", "2"], "create_p2");
workspace.run_br(["create", "P0 issue", "--priority", "0"], "create_p0");
workspace.run_bd(["create", "P0 issue", "--priority", "0"], "create_p0");
workspace.run_br(["create", "P4 issue", "--priority", "4"], "create_p4");
workspace.run_bd(["create", "P4 issue", "--priority", "4"], "create_p4");
let br_list = workspace.run_br(["list", "--sort", "priority", "--json"], "list_sort_pri");
let bd_list = workspace.run_bd(["list", "--sort", "priority", "--json"], "list_sort_pri");
assert!(
br_list.status.success(),
"br list sort priority failed: {}",
br_list.stderr
);
assert!(
bd_list.status.success(),
"bd list sort priority failed: {}",
bd_list.stderr
);
let br_list_json = extract_json_payload(&br_list.stdout);
let bd_list_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_list_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_list_json).unwrap_or(Value::Array(vec![]));
let br_priorities: Vec<i64> = br_val
.as_array()
.unwrap_or(&vec![])
.iter()
.filter_map(|v| v["priority"].as_i64())
.collect();
let bd_priorities: Vec<i64> = bd_val
.as_array()
.unwrap_or(&vec![])
.iter()
.filter_map(|v| v["priority"].as_i64())
.collect();
let mut br_sorted = br_priorities.clone();
br_sorted.sort();
let mut bd_sorted = bd_priorities.clone();
bd_sorted.sort();
assert_eq!(br_priorities, br_sorted, "br priorities not sorted");
assert_eq!(bd_priorities, bd_sorted, "bd priorities not sorted");
info!("conformance_list_sort_priority passed");
}
#[test]
#[ignore = "bd v0.46.0 list --sort created returns empty: known behavioral difference"]
fn conformance_list_sort_created() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_list_sort_created test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "First issue"], "create_first");
workspace.run_br_in_bd_env(["create", "First issue"], "create_first");
workspace.run_br(["create", "Second issue"], "create_second");
workspace.run_br_in_bd_env(["create", "Second issue"], "create_second");
let br_list = workspace.run_br(
["list", "--sort", "created_at", "--json"],
"list_sort_created",
);
let bd_list = workspace.run_bd(["list", "--sort", "created", "--json"], "list_sort_created");
assert!(
br_list.status.success(),
"br list sort created failed: {}",
br_list.stderr
);
assert!(
bd_list.status.success(),
"bd list sort created failed: {}",
bd_list.stderr
);
let br_list_json = extract_json_payload(&br_list.stdout);
let bd_list_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_list_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_list_json).unwrap_or(Value::Array(vec![]));
let br_titles: Vec<String> = br_val
.as_array()
.unwrap_or(&vec![])
.iter()
.filter_map(|v| v["title"].as_str().map(str::to_string))
.collect();
let bd_titles: Vec<String> = bd_val
.as_array()
.unwrap_or(&vec![])
.iter()
.filter_map(|v| v["title"].as_str().map(str::to_string))
.collect();
assert_eq!(
br_titles, bd_titles,
"created sort order differs: br={br_titles:?} bd={bd_titles:?}"
);
assert!(
bd_titles.first().is_some_and(|t| t == "Second issue"),
"bd created sort order unexpected: {bd_titles:?}"
);
info!("conformance_list_sort_created passed");
}
#[test]
fn conformance_list_json_structure() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_list_json_structure test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Structure issue"], "create");
workspace.run_br_in_bd_env(["create", "Structure issue"], "create");
let br_list = workspace.run_br(["list", "--json"], "list_struct");
let bd_list = workspace.run_bd(["list", "--json"], "list_struct");
assert!(
br_list.status.success(),
"br list struct failed: {}",
br_list.stderr
);
assert!(
bd_list.status.success(),
"bd list struct failed: {}",
bd_list.stderr
);
let br_list_json = extract_json_payload(&br_list.stdout);
let bd_list_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_list_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_list_json).unwrap_or(Value::Array(vec![]));
let br_item = br_val.as_array().and_then(|a| a.first());
let bd_item = bd_val.as_array().and_then(|a| a.first());
for item in [br_item, bd_item].into_iter().flatten() {
assert!(item.get("id").is_some(), "missing id in list item");
assert!(item.get("title").is_some(), "missing title in list item");
assert!(item.get("status").is_some(), "missing status in list item");
assert!(
item.get("priority").is_some(),
"missing priority in list item"
);
}
info!("conformance_list_json_structure passed");
}
#[test]
fn conformance_show_partial_id() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_show_partial_id test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Partial ID issue", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Partial ID issue", "--json"], "create");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
let br_hash = br_id.split('-').nth(1).unwrap_or(br_id);
let bd_hash = bd_id.split('-').nth(1).unwrap_or(bd_id);
let br_partial = &br_hash[..br_hash.len().min(6)];
let bd_partial = &bd_hash[..bd_hash.len().min(6)];
let br_show = workspace.run_br(["show", br_partial, "--json"], "show_partial");
let bd_show = workspace.run_bd(["show", bd_partial, "--json"], "show_partial");
assert!(
br_show.status.success(),
"br show partial failed: {}",
br_show.stderr
);
assert!(
bd_show.status.success(),
"bd show partial failed: {}",
bd_show.stderr
);
let br_show_json = extract_json_payload(&br_show.stdout);
let bd_show_json = extract_json_payload(&bd_show.stdout);
let result = compare_json(
&br_show_json,
&bd_show_json,
&CompareMode::ContainsFields(vec![
"title".to_string(),
"status".to_string(),
"issue_type".to_string(),
]),
);
assert!(
result.is_ok(),
"partial id show comparison failed: {:?}",
result.err()
);
info!("conformance_show_partial_id passed");
}
#[test]
fn conformance_show_nonexistent_error() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_show_nonexistent_error test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_show = workspace.run_br(["show", "bd-does-not-exist", "--json"], "show_missing");
let _bd_show = workspace.run_bd(["show", "bd-does-not-exist", "--json"], "show_missing");
assert!(
!br_show.status.success(),
"br expected show missing to fail"
);
info!("conformance_show_nonexistent_error passed");
}
#[test]
fn conformance_show_full_details() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_show_full_details test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(
[
"create",
"Full details issue",
"--type",
"feature",
"--priority",
"1",
"--assignee",
"alice",
"--description",
"Detail description",
"--external-ref",
"EXT-123",
"--json",
],
"create_full_details",
);
let bd_create = workspace.run_bd(
[
"create",
"Full details issue",
"--type",
"feature",
"--priority",
"1",
"--assignee",
"alice",
"--description",
"Detail description",
"--external-ref",
"EXT-123",
"--json",
],
"create_full_details",
);
assert!(
br_create.status.success(),
"br create full details failed: {}",
br_create.stderr
);
assert!(
bd_create.status.success(),
"bd create full details failed: {}",
bd_create.stderr
);
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
workspace.run_br(["label", "add", br_id, "urgent"], "label_add_full");
workspace.run_bd(["label", "add", bd_id, "urgent"], "label_add_full");
let br_show = workspace.run_br(["show", br_id, "--json"], "show_full");
let bd_show = workspace.run_bd(["show", bd_id, "--json"], "show_full");
assert!(
br_show.status.success(),
"br show full failed: {}",
br_show.stderr
);
assert!(
bd_show.status.success(),
"bd show full failed: {}",
bd_show.stderr
);
let br_show_json = extract_json_payload(&br_show.stdout);
let bd_show_json = extract_json_payload(&bd_show.stdout);
let result = compare_json(
&br_show_json,
&bd_show_json,
&CompareMode::ContainsFields(vec![
"title".to_string(),
"description".to_string(),
"assignee".to_string(),
"external_ref".to_string(),
"issue_type".to_string(),
"priority".to_string(),
]),
);
assert!(
result.is_ok(),
"full details comparison failed: {:?}",
result.err()
);
let br_val: Value = serde_json::from_str(&br_show_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_show_json).expect("parse");
let br_issue = if br_val.is_array() {
&br_val[0]
} else {
&br_val
};
let bd_issue = if bd_val.is_array() {
&bd_val[0]
} else {
&bd_val
};
for issue in [br_issue, bd_issue] {
assert!(issue.get("labels").is_some(), "missing labels");
}
info!("conformance_show_full_details passed");
}
#[test]
fn conformance_show_with_dependencies() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_show_with_dependencies test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_parent = workspace.run_br(["create", "Parent issue", "--json"], "create_parent");
let bd_parent = workspace.run_bd(["create", "Parent issue", "--json"], "create_parent");
let br_child = workspace.run_br(["create", "Child issue", "--json"], "create_child");
let bd_child = workspace.run_bd(["create", "Child issue", "--json"], "create_child");
let br_parent_json = extract_json_payload(&br_parent.stdout);
let bd_parent_json = extract_json_payload(&bd_parent.stdout);
let br_child_json = extract_json_payload(&br_child.stdout);
let bd_child_json = extract_json_payload(&bd_child.stdout);
let br_parent_val: Value = serde_json::from_str(&br_parent_json).expect("parse");
let bd_parent_val: Value = serde_json::from_str(&bd_parent_json).expect("parse");
let br_child_val: Value = serde_json::from_str(&br_child_json).expect("parse");
let bd_child_val: Value = serde_json::from_str(&bd_child_json).expect("parse");
let br_parent_id = br_parent_val["id"]
.as_str()
.or_else(|| br_parent_val[0]["id"].as_str())
.unwrap();
let bd_parent_id = bd_parent_val["id"]
.as_str()
.or_else(|| bd_parent_val[0]["id"].as_str())
.unwrap();
let br_child_id = br_child_val["id"]
.as_str()
.or_else(|| br_child_val[0]["id"].as_str())
.unwrap();
let bd_child_id = bd_child_val["id"]
.as_str()
.or_else(|| bd_child_val[0]["id"].as_str())
.unwrap();
workspace.run_br(["dep", "add", br_child_id, br_parent_id], "dep_add");
workspace.run_bd(["dep", "add", bd_child_id, bd_parent_id], "dep_add");
let br_show = workspace.run_br(["show", br_child_id, "--json"], "show_deps");
let bd_show = workspace.run_bd(["show", bd_child_id, "--json"], "show_deps");
assert!(
br_show.status.success(),
"br show deps failed: {}",
br_show.stderr
);
assert!(
bd_show.status.success(),
"bd show deps failed: {}",
bd_show.stderr
);
let br_show_json = extract_json_payload(&br_show.stdout);
let bd_show_json = extract_json_payload(&bd_show.stdout);
let br_val: Value = serde_json::from_str(&br_show_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_show_json).expect("parse");
let br_issue = if br_val.is_array() {
&br_val[0]
} else {
&br_val
};
let bd_issue = if bd_val.is_array() {
&bd_val[0]
} else {
&bd_val
};
let br_len = br_issue["dependencies"]
.as_array()
.map(|a| a.len())
.unwrap_or(0);
let bd_len = bd_issue["dependencies"]
.as_array()
.map(|a| a.len())
.unwrap_or(0);
assert_eq!(
br_len, bd_len,
"dependency counts differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 1, "expected 1 dependency");
info!("conformance_show_with_dependencies passed");
}
#[test]
fn conformance_show_with_comments() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_show_with_comments test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Commented issue", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Commented issue", "--json"], "create");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
let comment_text = "First comment";
workspace.run_br(["comments", "add", br_id, comment_text], "comment_add");
workspace.run_bd(["comments", "add", bd_id, comment_text], "comment_add");
let br_show = workspace.run_br(["show", br_id, "--json"], "show_comments");
let bd_show = workspace.run_bd(["show", bd_id, "--json"], "show_comments");
assert!(
br_show.status.success(),
"br show comments failed: {}",
br_show.stderr
);
assert!(
bd_show.status.success(),
"bd show comments failed: {}",
bd_show.stderr
);
let br_show_json = extract_json_payload(&br_show.stdout);
let bd_show_json = extract_json_payload(&bd_show.stdout);
let br_val: Value = serde_json::from_str(&br_show_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_show_json).expect("parse");
let br_issue = if br_val.is_array() {
&br_val[0]
} else {
&br_val
};
let bd_issue = if bd_val.is_array() {
&bd_val[0]
} else {
&bd_val
};
let br_len = br_issue["comments"]
.as_array()
.map(|a| a.len())
.unwrap_or(0);
let bd_len = bd_issue["comments"]
.as_array()
.map(|a| a.len())
.unwrap_or(0);
assert_eq!(
br_len, bd_len,
"comment counts differ: br={}, bd={}",
br_len, bd_len
);
assert_eq!(br_len, 1, "expected 1 comment");
info!("conformance_show_with_comments passed");
}
#[test]
#[ignore]
fn conformance_show_deleted_issue() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_show_deleted_issue test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Deleted issue", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Deleted issue", "--json"], "create");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
workspace.run_br(["delete", br_id, "--reason", "cleanup"], "delete");
workspace.run_bd(
["delete", bd_id, "--reason", "cleanup", "--force"],
"delete",
);
let br_show = workspace.run_br(["show", br_id, "--json"], "show_deleted");
let bd_show = workspace.run_bd(["show", bd_id, "--json"], "show_deleted");
assert_eq!(
br_show.status.success(),
bd_show.status.success(),
"show deleted behavior differs: br success={}, bd success={}",
br_show.status.success(),
bd_show.status.success()
);
if br_show.status.success() && bd_show.status.success() {
let br_show_json = extract_json_payload(&br_show.stdout);
let bd_show_json = extract_json_payload(&bd_show.stdout);
if br_show_json.trim().is_empty() || bd_show_json.trim().is_empty() {
assert!(
br_show_json.trim().is_empty() && bd_show_json.trim().is_empty(),
"deleted show output mismatch: br='{}' bd='{}'",
br_show_json,
bd_show_json
);
} else {
let br_val: Value = serde_json::from_str(&br_show_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_show_json).expect("parse");
let br_issue = if br_val.is_array() {
&br_val[0]
} else {
&br_val
};
let bd_issue = if bd_val.is_array() {
&bd_val[0]
} else {
&bd_val
};
assert_eq!(
br_issue["status"].as_str(),
bd_issue["status"].as_str(),
"deleted status mismatch"
);
}
}
info!("conformance_show_deleted_issue passed");
}
#[test]
fn conformance_update_title() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_update_title test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Old title", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Old title", "--json"], "create");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
let br_update = workspace.run_br(
["update", br_id, "--title", "New title", "--json"],
"update_title",
);
let bd_update = workspace.run_bd(
["update", bd_id, "--title", "New title", "--json"],
"update_title",
);
assert!(
br_update.status.success(),
"br update title failed: {}",
br_update.stderr
);
assert!(
bd_update.status.success(),
"bd update title failed: {}",
bd_update.stderr
);
let br_show = workspace.run_br(["show", br_id, "--json"], "show_after_update");
let bd_show = workspace.run_bd(["show", bd_id, "--json"], "show_after_update");
let br_show_json = extract_json_payload(&br_show.stdout);
let bd_show_json = extract_json_payload(&bd_show.stdout);
let br_val: Value = serde_json::from_str(&br_show_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_show_json).expect("parse");
let br_title = br_val["title"]
.as_str()
.or_else(|| br_val[0]["title"].as_str());
let bd_title = bd_val["title"]
.as_str()
.or_else(|| bd_val[0]["title"].as_str());
assert_eq!(
br_title, bd_title,
"title mismatch after update: br={:?}, bd={:?}",
br_title, bd_title
);
assert_eq!(br_title, Some("New title"), "expected updated title");
info!("conformance_update_title passed");
}
#[test]
fn conformance_update_assignee() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_update_assignee test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Assignee update", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Assignee update", "--json"], "create");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
let br_update = workspace.run_br(
["update", br_id, "--assignee", "alice", "--json"],
"update_assignee",
);
let bd_update = workspace.run_bd(
["update", bd_id, "--assignee", "alice", "--json"],
"update_assignee",
);
assert!(
br_update.status.success(),
"br update assignee failed: {}",
br_update.stderr
);
assert!(
bd_update.status.success(),
"bd update assignee failed: {}",
bd_update.stderr
);
let br_show = workspace.run_br(["show", br_id, "--json"], "show_after_assignee");
let bd_show = workspace.run_bd(["show", bd_id, "--json"], "show_after_assignee");
let br_show_json = extract_json_payload(&br_show.stdout);
let bd_show_json = extract_json_payload(&bd_show.stdout);
let br_val: Value = serde_json::from_str(&br_show_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_show_json).expect("parse");
let br_assignee = br_val["assignee"]
.as_str()
.or_else(|| br_val[0]["assignee"].as_str());
let bd_assignee = bd_val["assignee"]
.as_str()
.or_else(|| bd_val[0]["assignee"].as_str());
assert_eq!(
br_assignee, bd_assignee,
"assignee mismatch after update: br={:?}, bd={:?}",
br_assignee, bd_assignee
);
assert_eq!(br_assignee, Some("alice"), "expected assignee alice");
info!("conformance_update_assignee passed");
}
#[test]
fn conformance_update_status() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_update_status test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Status issue", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Status issue", "--json"], "create");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
let br_update = workspace.run_br(
["update", br_id, "--status", "in_progress", "--json"],
"update_status",
);
let bd_update = workspace.run_bd(
["update", bd_id, "--status", "in_progress", "--json"],
"update_status",
);
assert!(
br_update.status.success(),
"br update status failed: {}",
br_update.stderr
);
assert!(
bd_update.status.success(),
"bd update status failed: {}",
bd_update.stderr
);
let br_show = workspace.run_br(["show", br_id, "--json"], "show_after_status");
let bd_show = workspace.run_bd(["show", bd_id, "--json"], "show_after_status");
let br_show_json = extract_json_payload(&br_show.stdout);
let bd_show_json = extract_json_payload(&bd_show.stdout);
let br_val: Value = serde_json::from_str(&br_show_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_show_json).expect("parse");
let br_status = br_val["status"]
.as_str()
.or_else(|| br_val[0]["status"].as_str());
let bd_status = bd_val["status"]
.as_str()
.or_else(|| bd_val[0]["status"].as_str());
assert_eq!(
br_status, bd_status,
"status mismatch after update: br={:?}, bd={:?}",
br_status, bd_status
);
assert_eq!(
br_status,
Some("in_progress"),
"expected status in_progress"
);
info!("conformance_update_status passed");
}
#[test]
fn conformance_update_multiple_fields() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_update_multiple_fields test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Multi update", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Multi update", "--json"], "create");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
let br_update = workspace.run_br(
[
"update",
br_id,
"--title",
"Updated title",
"--priority",
"0",
"--assignee",
"bob",
"--type",
"bug",
"--description",
"Updated description",
"--json",
],
"update_multi",
);
let bd_update = workspace.run_bd(
[
"update",
bd_id,
"--title",
"Updated title",
"--priority",
"0",
"--assignee",
"bob",
"--type",
"bug",
"--description",
"Updated description",
"--json",
],
"update_multi",
);
assert!(
br_update.status.success(),
"br update multi failed: {}",
br_update.stderr
);
assert!(
bd_update.status.success(),
"bd update multi failed: {}",
bd_update.stderr
);
let br_show = workspace.run_br(["show", br_id, "--json"], "show_multi");
let bd_show = workspace.run_bd(["show", bd_id, "--json"], "show_multi");
let br_show_json = extract_json_payload(&br_show.stdout);
let bd_show_json = extract_json_payload(&bd_show.stdout);
let result = compare_json(
&br_show_json,
&bd_show_json,
&CompareMode::ContainsFields(vec![
"title".to_string(),
"priority".to_string(),
"assignee".to_string(),
"issue_type".to_string(),
"description".to_string(),
]),
);
assert!(
result.is_ok(),
"multi update comparison failed: {:?}",
result.err()
);
info!("conformance_update_multiple_fields passed");
}
#[test]
fn conformance_update_clear_assignee() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_update_clear_assignee test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(
["create", "Assignee clear", "--assignee", "alice", "--json"],
"create",
);
let bd_create = workspace.run_bd(
["create", "Assignee clear", "--assignee", "alice", "--json"],
"create",
);
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
let br_update = workspace.run_br(
["update", br_id, "--assignee", "", "--json"],
"update_clear_assignee",
);
let bd_update = workspace.run_bd(
["update", bd_id, "--assignee", "", "--json"],
"update_clear_assignee",
);
assert!(
br_update.status.success(),
"br update clear assignee failed: {}",
br_update.stderr
);
assert!(
bd_update.status.success(),
"bd update clear assignee failed: {}",
bd_update.stderr
);
let br_show = workspace.run_br(["show", br_id, "--json"], "show_clear_assignee");
let bd_show = workspace.run_bd(["show", bd_id, "--json"], "show_clear_assignee");
let br_show_json = extract_json_payload(&br_show.stdout);
let bd_show_json = extract_json_payload(&bd_show.stdout);
let br_val: Value = serde_json::from_str(&br_show_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_show_json).expect("parse");
let br_issue = if br_val.is_array() {
&br_val[0]
} else {
&br_val
};
let bd_issue = if bd_val.is_array() {
&bd_val[0]
} else {
&bd_val
};
let br_assignee = br_issue.get("assignee").and_then(|v| v.as_str());
let bd_assignee = bd_issue.get("assignee").and_then(|v| v.as_str());
assert_eq!(
br_assignee, bd_assignee,
"assignee mismatch after clear: br={:?}, bd={:?}",
br_assignee, bd_assignee
);
assert!(br_assignee.is_none(), "expected assignee cleared");
info!("conformance_update_clear_assignee passed");
}
#[test]
fn conformance_update_preserves_other_fields() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_update_preserves_other_fields test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(
[
"create",
"Preserve fields",
"--description",
"Keep me",
"--external-ref",
"EXT-999",
"--json",
],
"create",
);
let bd_create = workspace.run_bd(
[
"create",
"Preserve fields",
"--description",
"Keep me",
"--external-ref",
"EXT-999",
"--json",
],
"create",
);
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
workspace.run_br(["update", br_id, "--priority", "0"], "update_pri");
workspace.run_bd(["update", bd_id, "--priority", "0"], "update_pri");
let br_show = workspace.run_br(["show", br_id, "--json"], "show_preserve");
let bd_show = workspace.run_bd(["show", bd_id, "--json"], "show_preserve");
let br_show_json = extract_json_payload(&br_show.stdout);
let bd_show_json = extract_json_payload(&bd_show.stdout);
let br_val: Value = serde_json::from_str(&br_show_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_show_json).expect("parse");
let br_issue = if br_val.is_array() {
&br_val[0]
} else {
&br_val
};
let bd_issue = if bd_val.is_array() {
&bd_val[0]
} else {
&bd_val
};
assert_eq!(
br_issue["description"].as_str(),
bd_issue["description"].as_str(),
"description mismatch after update"
);
assert_eq!(
br_issue["external_ref"].as_str(),
bd_issue["external_ref"].as_str(),
"external_ref mismatch after update"
);
assert_eq!(
br_issue["description"].as_str(),
Some("Keep me"),
"description should be preserved"
);
info!("conformance_update_preserves_other_fields passed");
}
#[test]
fn conformance_update_nonexistent_error() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_update_nonexistent_error test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_update = workspace.run_br(
["update", "bd-does-not-exist", "--title", "Nope", "--json"],
"update_missing",
);
let _bd_update = workspace.run_bd(
["update", "bd-does-not-exist", "--title", "Nope", "--json"],
"update_missing",
);
assert!(
!br_update.status.success(),
"br expected update missing to fail"
);
info!("conformance_update_nonexistent_error passed");
}
#[test]
fn conformance_close_with_reason() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_close_with_reason test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Close reason issue", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Close reason issue", "--json"], "create");
let br_json = extract_json_payload(&br_create.stdout);
let bd_json = extract_json_payload(&bd_create.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse");
let br_id = br_val["id"]
.as_str()
.or_else(|| br_val[0]["id"].as_str())
.unwrap();
let bd_id = bd_val["id"]
.as_str()
.or_else(|| bd_val[0]["id"].as_str())
.unwrap();
let br_close = workspace.run_br(
["close", br_id, "--reason", "done", "--json"],
"close_reason",
);
let bd_close = workspace.run_bd(
["close", bd_id, "--reason", "done", "--json"],
"close_reason",
);
assert!(
br_close.status.success(),
"br close with reason failed: {}",
br_close.stderr
);
assert!(
bd_close.status.success(),
"bd close with reason failed: {}",
bd_close.stderr
);
let br_close_json = extract_json_payload(&br_close.stdout);
let bd_close_json = extract_json_payload(&bd_close.stdout);
let br_val: Value = serde_json::from_str(&br_close_json).expect("parse");
let bd_val: Value = serde_json::from_str(&bd_close_json).expect("parse");
let br_reason = br_val["close_reason"]
.as_str()
.or_else(|| br_val[0]["close_reason"].as_str());
let bd_reason = bd_val["close_reason"]
.as_str()
.or_else(|| bd_val[0]["close_reason"].as_str());
assert_eq!(
br_reason, bd_reason,
"close_reason mismatch: br={:?}, bd={:?}",
br_reason, bd_reason
);
assert_eq!(br_reason, Some("done"), "expected close reason");
info!("conformance_close_with_reason passed");
}
fn extract_issue_id(json_str: &str) -> String {
let val: Value = serde_json::from_str(json_str).expect("parse json");
val["id"]
.as_str()
.or_else(|| val[0]["id"].as_str())
.expect("id field")
.to_string()
}
fn extract_id_from_json(output: &str) -> String {
let json = extract_json_payload(output);
extract_issue_id(&json)
}
fn assert_command_success(output: &CmdOutput, label: &str) {
assert!(
output.status.success(),
"{label} failed: stdout='{}' stderr='{}'",
output.stdout,
output.stderr
);
}
fn issue_value_from_show_output(output: &CmdOutput, label: &str) -> Value {
assert_command_success(output, label);
let json = extract_json_payload(&output.stdout);
let value: Value = serde_json::from_str(&json).expect("parse show json");
if value.is_array() {
value[0].clone()
} else {
value
}
}
fn assert_json_output_mentions(output: &CmdOutput, expected: &str, label: &str) {
assert_command_success(output, label);
let json = extract_json_payload(&output.stdout);
assert!(
json.contains(expected),
"{label} output should mention {expected}: {json}"
);
}
fn extract_checks_len(json_str: &str) -> usize {
serde_json::from_str::<Value>(json_str)
.ok()
.and_then(|val| {
val.get("checks")
.and_then(|checks| checks.as_array())
.map(|checks| checks.len())
})
.unwrap_or(0)
}
#[test]
fn conformance_dep_add_basic() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_add_basic test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_blocker = workspace.run_br(["create", "Blocker issue", "--json"], "create_blocker");
let bd_blocker = workspace.run_bd(["create", "Blocker issue", "--json"], "create_blocker");
let br_dependent =
workspace.run_br(["create", "Dependent issue", "--json"], "create_dependent");
let bd_dependent =
workspace.run_bd(["create", "Dependent issue", "--json"], "create_dependent");
let br_blocker_id = extract_issue_id(&extract_json_payload(&br_blocker.stdout));
let bd_blocker_id = extract_issue_id(&extract_json_payload(&bd_blocker.stdout));
let br_dependent_id = extract_issue_id(&extract_json_payload(&br_dependent.stdout));
let bd_dependent_id = extract_issue_id(&extract_json_payload(&bd_dependent.stdout));
let br_add = workspace.run_br(
["dep", "add", &br_dependent_id, &br_blocker_id, "--json"],
"dep_add",
);
let bd_add = workspace.run_bd(
["dep", "add", &bd_dependent_id, &bd_blocker_id, "--json"],
"dep_add",
);
assert!(
br_add.status.success(),
"br dep add failed: {}",
br_add.stderr
);
assert!(
bd_add.status.success(),
"bd dep add failed: {}",
bd_add.stderr
);
let br_json = extract_json_payload(&br_add.stdout);
let bd_json = extract_json_payload(&bd_add.stdout);
let br_val: Value = serde_json::from_str(&br_json).unwrap_or(Value::Null);
let bd_val: Value = serde_json::from_str(&bd_json).unwrap_or(Value::Null);
let br_status = br_val["status"].as_str().or(br_val["action"].as_str());
let bd_status = bd_val["status"].as_str().or(bd_val["action"].as_str());
assert!(
br_status.is_some() || br_add.status.success(),
"br should indicate success"
);
assert!(
bd_status.is_some() || bd_add.status.success(),
"bd should indicate success"
);
info!("conformance_dep_add_basic passed");
}
#[test]
fn conformance_dep_add_all_types() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_add_all_types test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let dep_types = [
"blocks",
"parent-child",
"related",
"discovered-from",
"replies-to",
"relates-to",
"duplicates",
"supersedes",
"caused-by",
];
for dep_type in dep_types {
let br_source = workspace.run_br(
["create", &format!("Source for {}", dep_type), "--json"],
&format!("create_source_{}", dep_type),
);
let bd_source = workspace.run_bd(
["create", &format!("Source for {}", dep_type), "--json"],
&format!("create_source_{}", dep_type),
);
let br_target = workspace.run_br(
["create", &format!("Target for {}", dep_type), "--json"],
&format!("create_target_{}", dep_type),
);
let bd_target = workspace.run_bd(
["create", &format!("Target for {}", dep_type), "--json"],
&format!("create_target_{}", dep_type),
);
let br_source_id = extract_issue_id(&extract_json_payload(&br_source.stdout));
let bd_source_id = extract_issue_id(&extract_json_payload(&bd_source.stdout));
let br_target_id = extract_issue_id(&extract_json_payload(&br_target.stdout));
let bd_target_id = extract_issue_id(&extract_json_payload(&bd_target.stdout));
let br_add = workspace.run_br(
["dep", "add", &br_source_id, &br_target_id, "-t", dep_type],
&format!("dep_add_{}", dep_type),
);
let bd_add = workspace.run_bd(
["dep", "add", &bd_source_id, &bd_target_id, "-t", dep_type],
&format!("dep_add_{}", dep_type),
);
assert!(
br_add.status.success(),
"br dep add failed for type '{}': {}",
dep_type,
br_add.stderr
);
assert!(
bd_add.status.success(),
"bd dep add failed for type '{}': {}",
dep_type,
bd_add.stderr
);
}
info!("conformance_dep_add_all_types passed");
}
#[test]
fn conformance_dep_add_duplicate() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_add_duplicate test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "Issue A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "Issue A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "Issue B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "Issue B", "--json"], "create_b");
let br_a_id = extract_issue_id(&extract_json_payload(&br_a.stdout));
let bd_a_id = extract_issue_id(&extract_json_payload(&bd_a.stdout));
let br_b_id = extract_issue_id(&extract_json_payload(&br_b.stdout));
let bd_b_id = extract_issue_id(&extract_json_payload(&bd_b.stdout));
let br_add1 = workspace.run_br(["dep", "add", &br_a_id, &br_b_id], "dep_add_1");
let bd_add1 = workspace.run_bd(["dep", "add", &bd_a_id, &bd_b_id], "dep_add_1");
assert!(br_add1.status.success(), "br first dep add failed");
assert!(bd_add1.status.success(), "bd first dep add failed");
let br_add2 = workspace.run_br(["dep", "add", &br_a_id, &br_b_id, "--json"], "dep_add_2");
let bd_add2 = workspace.run_bd(["dep", "add", &bd_a_id, &bd_b_id, "--json"], "dep_add_2");
info!(
"Duplicate dep handling: br={}, bd={} (known difference: br is idempotent)",
br_add2.status.success(),
bd_add2.status.success()
);
assert!(
br_add2.status.success(),
"br should succeed on duplicate dep add (idempotent behavior)"
);
info!("conformance_dep_add_duplicate passed");
}
#[test]
fn conformance_dep_add_self_reference_error() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_add_self_reference_error test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_issue = workspace.run_br(["create", "Self-ref test", "--json"], "create");
let bd_issue = workspace.run_bd(["create", "Self-ref test", "--json"], "create");
let br_id = extract_issue_id(&extract_json_payload(&br_issue.stdout));
let bd_id = extract_issue_id(&extract_json_payload(&bd_issue.stdout));
let br_add = workspace.run_br(["dep", "add", &br_id, &br_id], "dep_add_self");
let bd_add = workspace.run_bd(["dep", "add", &bd_id, &bd_id], "dep_add_self");
assert!(
!br_add.status.success(),
"br should reject self-dependency but it succeeded"
);
assert!(
!bd_add.status.success(),
"bd should reject self-dependency but it succeeded"
);
info!("conformance_dep_add_self_reference_error passed");
}
#[test]
fn conformance_dep_add_cycle_detection() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_add_cycle_detection test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "Cycle A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "Cycle A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "Cycle B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "Cycle B", "--json"], "create_b");
let br_a_id = extract_issue_id(&extract_json_payload(&br_a.stdout));
let bd_a_id = extract_issue_id(&extract_json_payload(&bd_a.stdout));
let br_b_id = extract_issue_id(&extract_json_payload(&br_b.stdout));
let bd_b_id = extract_issue_id(&extract_json_payload(&bd_b.stdout));
let br_add1 = workspace.run_br(["dep", "add", &br_a_id, &br_b_id], "add_a_to_b");
let bd_add1 = workspace.run_bd(["dep", "add", &bd_a_id, &bd_b_id], "add_a_to_b");
assert!(br_add1.status.success(), "br first dep failed");
assert!(bd_add1.status.success(), "bd first dep failed");
let br_add2 = workspace.run_br(["dep", "add", &br_b_id, &br_a_id], "add_b_to_a");
let bd_add2 = workspace.run_bd(["dep", "add", &bd_b_id, &bd_a_id], "add_b_to_a");
assert!(
!br_add2.status.success(),
"br should reject cycle A->B->A but succeeded"
);
assert!(
!bd_add2.status.success(),
"bd should reject cycle A->B->A but succeeded"
);
info!("conformance_dep_add_cycle_detection passed");
}
#[test]
fn conformance_dep_add_transitive_cycle() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_add_transitive_cycle test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "Trans A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "Trans A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "Trans B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "Trans B", "--json"], "create_b");
let br_c = workspace.run_br(["create", "Trans C", "--json"], "create_c");
let bd_c = workspace.run_bd(["create", "Trans C", "--json"], "create_c");
let br_a_id = extract_issue_id(&extract_json_payload(&br_a.stdout));
let bd_a_id = extract_issue_id(&extract_json_payload(&bd_a.stdout));
let br_b_id = extract_issue_id(&extract_json_payload(&br_b.stdout));
let bd_b_id = extract_issue_id(&extract_json_payload(&bd_b.stdout));
let br_c_id = extract_issue_id(&extract_json_payload(&br_c.stdout));
let bd_c_id = extract_issue_id(&extract_json_payload(&bd_c.stdout));
let br_ab = workspace.run_br(["dep", "add", &br_a_id, &br_b_id], "add_a_b");
let bd_ab = workspace.run_bd(["dep", "add", &bd_a_id, &bd_b_id], "add_a_b");
assert!(br_ab.status.success());
assert!(bd_ab.status.success());
let br_bc = workspace.run_br(["dep", "add", &br_b_id, &br_c_id], "add_b_c");
let bd_bc = workspace.run_bd(["dep", "add", &bd_b_id, &bd_c_id], "add_b_c");
assert!(br_bc.status.success());
assert!(bd_bc.status.success());
let br_ca = workspace.run_br(["dep", "add", &br_c_id, &br_a_id], "add_c_a");
let bd_ca = workspace.run_bd(["dep", "add", &bd_c_id, &bd_a_id], "add_c_a");
assert!(
!br_ca.status.success(),
"br should reject transitive cycle A->B->C->A"
);
assert!(
!bd_ca.status.success(),
"bd should reject transitive cycle A->B->C->A"
);
info!("conformance_dep_add_transitive_cycle passed");
}
#[test]
fn conformance_dep_add_nonexistent_source_error() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_add_nonexistent_source_error test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_target = workspace.run_br(["create", "Target issue", "--json"], "create_target");
let bd_target = workspace.run_bd(["create", "Target issue", "--json"], "create_target");
let br_target_id = extract_issue_id(&extract_json_payload(&br_target.stdout));
let bd_target_id = extract_issue_id(&extract_json_payload(&bd_target.stdout));
let br_add = workspace.run_br(
["dep", "add", "bd-nonexistent999", &br_target_id],
"dep_add",
);
let bd_add = workspace.run_bd(
["dep", "add", "bd-nonexistent999", &bd_target_id],
"dep_add",
);
assert!(
!br_add.status.success(),
"br should reject nonexistent source"
);
assert!(
!bd_add.status.success(),
"bd should reject nonexistent source"
);
info!("conformance_dep_add_nonexistent_source_error passed");
}
#[test]
fn conformance_dep_add_nonexistent_target_error() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_add_nonexistent_target_error test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_source = workspace.run_br(["create", "Source issue", "--json"], "create_source");
let bd_source = workspace.run_bd(["create", "Source issue", "--json"], "create_source");
let br_source_id = extract_issue_id(&extract_json_payload(&br_source.stdout));
let bd_source_id = extract_issue_id(&extract_json_payload(&bd_source.stdout));
let br_add = workspace.run_br(
["dep", "add", &br_source_id, "bd-nonexistent999"],
"dep_add",
);
let bd_add = workspace.run_bd(
["dep", "add", &bd_source_id, "bd-nonexistent999"],
"dep_add",
);
assert!(
!br_add.status.success(),
"br should reject nonexistent target"
);
assert!(
!bd_add.status.success(),
"bd should reject nonexistent target"
);
info!("conformance_dep_add_nonexistent_target_error passed");
}
#[test]
fn conformance_dep_remove_basic_expanded() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_remove_basic_expanded test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "Remove A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "Remove A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "Remove B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "Remove B", "--json"], "create_b");
let br_a_id = extract_issue_id(&extract_json_payload(&br_a.stdout));
let bd_a_id = extract_issue_id(&extract_json_payload(&bd_a.stdout));
let br_b_id = extract_issue_id(&extract_json_payload(&br_b.stdout));
let bd_b_id = extract_issue_id(&extract_json_payload(&bd_b.stdout));
workspace.run_br(["dep", "add", &br_a_id, &br_b_id], "add_dep");
workspace.run_bd(["dep", "add", &bd_a_id, &bd_b_id], "add_dep");
let br_rm = workspace.run_br(["dep", "remove", &br_a_id, &br_b_id, "--json"], "rm_dep");
let bd_rm = workspace.run_bd(["dep", "remove", &bd_a_id, &bd_b_id, "--json"], "rm_dep");
assert!(
br_rm.status.success(),
"br dep remove failed: {}",
br_rm.stderr
);
assert!(
bd_rm.status.success(),
"bd dep remove failed: {}",
bd_rm.stderr
);
let br_list = workspace.run_br(["dep", "list", &br_a_id, "--json"], "list_after");
let bd_list = workspace.run_bd(["dep", "list", &bd_a_id, "--json"], "list_after");
let br_json = extract_json_payload(&br_list.stdout);
let bd_json = extract_json_payload(&bd_list.stdout);
let br_deps: Value = serde_json::from_str(&br_json).unwrap_or(Value::Array(vec![]));
let bd_deps: Value = serde_json::from_str(&bd_json).unwrap_or(Value::Array(vec![]));
let br_len = br_deps.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_deps.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(br_len, 0, "br should have 0 deps after remove");
assert_eq!(bd_len, 0, "bd should have 0 deps after remove");
info!("conformance_dep_remove_basic_expanded passed");
}
#[test]
fn conformance_dep_remove_nonexistent() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_remove_nonexistent test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "No-dep A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "No-dep A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "No-dep B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "No-dep B", "--json"], "create_b");
let br_a_id = extract_issue_id(&extract_json_payload(&br_a.stdout));
let bd_a_id = extract_issue_id(&extract_json_payload(&bd_a.stdout));
let br_b_id = extract_issue_id(&extract_json_payload(&br_b.stdout));
let bd_b_id = extract_issue_id(&extract_json_payload(&bd_b.stdout));
let br_rm = workspace.run_br(
["dep", "remove", &br_a_id, &br_b_id, "--json"],
"rm_nonexistent",
);
let bd_rm = workspace.run_bd(
["dep", "remove", &bd_a_id, &bd_b_id, "--json"],
"rm_nonexistent",
);
info!(
"Remove nonexistent dep: br={}, bd={} (known difference: br is idempotent)",
br_rm.status.success(),
bd_rm.status.success()
);
assert!(
br_rm.status.success(),
"br should succeed on removing nonexistent dep (idempotent behavior)"
);
info!("conformance_dep_remove_nonexistent passed");
}
#[test]
fn conformance_dep_remove_unblocks_issue() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_remove_unblocks_issue test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_blocker = workspace.run_br(["create", "Blocker", "--json"], "create_blocker");
let bd_blocker = workspace.run_bd(["create", "Blocker", "--json"], "create_blocker");
let br_blocked = workspace.run_br(["create", "Blocked", "--json"], "create_blocked");
let bd_blocked = workspace.run_bd(["create", "Blocked", "--json"], "create_blocked");
let br_blocker_id = extract_issue_id(&extract_json_payload(&br_blocker.stdout));
let bd_blocker_id = extract_issue_id(&extract_json_payload(&bd_blocker.stdout));
let br_blocked_id = extract_issue_id(&extract_json_payload(&br_blocked.stdout));
let bd_blocked_id = extract_issue_id(&extract_json_payload(&bd_blocked.stdout));
workspace.run_br(["dep", "add", &br_blocked_id, &br_blocker_id], "add_dep");
workspace.run_bd(["dep", "add", &bd_blocked_id, &bd_blocker_id], "add_dep");
let br_blocked_before = workspace.run_br(["blocked", "--json"], "blocked_before");
let bd_blocked_before = workspace.run_bd(["blocked", "--json"], "blocked_before");
let br_before: Value = serde_json::from_str(&extract_json_payload(&br_blocked_before.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_before: Value = serde_json::from_str(&extract_json_payload(&bd_blocked_before.stdout))
.unwrap_or(Value::Array(vec![]));
assert_eq!(
br_before.as_array().map(|a| a.len()).unwrap_or(0),
1,
"br should have 1 blocked"
);
assert_eq!(
bd_before.as_array().map(|a| a.len()).unwrap_or(0),
1,
"bd should have 1 blocked"
);
workspace.run_br(["dep", "remove", &br_blocked_id, &br_blocker_id], "rm_dep");
workspace.run_bd(["dep", "remove", &bd_blocked_id, &bd_blocker_id], "rm_dep");
let br_blocked_after = workspace.run_br(["blocked", "--json"], "blocked_after");
let bd_blocked_after = workspace.run_bd(["blocked", "--json"], "blocked_after");
let br_after: Value = serde_json::from_str(&extract_json_payload(&br_blocked_after.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_after: Value = serde_json::from_str(&extract_json_payload(&bd_blocked_after.stdout))
.unwrap_or(Value::Array(vec![]));
assert_eq!(
br_after.as_array().map(|a| a.len()).unwrap_or(0),
0,
"br should have 0 blocked"
);
assert_eq!(
bd_after.as_array().map(|a| a.len()).unwrap_or(0),
0,
"bd should have 0 blocked"
);
let br_ready = workspace.run_br(["ready", "--json"], "ready_after");
let bd_ready = workspace.run_bd(["ready", "--json"], "ready_after");
let br_ready_val: Value = serde_json::from_str(&extract_json_payload(&br_ready.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_ready_val: Value = serde_json::from_str(&extract_json_payload(&bd_ready.stdout))
.unwrap_or(Value::Array(vec![]));
assert_eq!(
br_ready_val.as_array().map(|a| a.len()).unwrap_or(0),
bd_ready_val.as_array().map(|a| a.len()).unwrap_or(0),
"ready counts should match"
);
info!("conformance_dep_remove_unblocks_issue passed");
}
#[test]
fn conformance_dep_remove_preserves_other_deps() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_remove_preserves_other_deps test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "Multi A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "Multi A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "Multi B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "Multi B", "--json"], "create_b");
let br_c = workspace.run_br(["create", "Multi C", "--json"], "create_c");
let bd_c = workspace.run_bd(["create", "Multi C", "--json"], "create_c");
let br_a_id = extract_issue_id(&extract_json_payload(&br_a.stdout));
let bd_a_id = extract_issue_id(&extract_json_payload(&bd_a.stdout));
let br_b_id = extract_issue_id(&extract_json_payload(&br_b.stdout));
let bd_b_id = extract_issue_id(&extract_json_payload(&bd_b.stdout));
let br_c_id = extract_issue_id(&extract_json_payload(&br_c.stdout));
let bd_c_id = extract_issue_id(&extract_json_payload(&bd_c.stdout));
workspace.run_br(
["dep", "add", &br_a_id, &br_b_id, "-t", "related"],
"add_a_b",
);
workspace.run_bd(
["dep", "add", &bd_a_id, &bd_b_id, "-t", "related"],
"add_a_b",
);
workspace.run_br(
["dep", "add", &br_a_id, &br_c_id, "-t", "related"],
"add_a_c",
);
workspace.run_bd(
["dep", "add", &bd_a_id, &bd_c_id, "-t", "related"],
"add_a_c",
);
let br_list_before = workspace.run_br(["dep", "list", &br_a_id, "--json"], "list_before");
let bd_list_before = workspace.run_bd(["dep", "list", &bd_a_id, "--json"], "list_before");
let br_before: Value = serde_json::from_str(&extract_json_payload(&br_list_before.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_before: Value = serde_json::from_str(&extract_json_payload(&bd_list_before.stdout))
.unwrap_or(Value::Array(vec![]));
assert_eq!(br_before.as_array().map(|a| a.len()).unwrap_or(0), 2);
assert_eq!(bd_before.as_array().map(|a| a.len()).unwrap_or(0), 2);
workspace.run_br(["dep", "remove", &br_a_id, &br_b_id], "rm_a_b");
workspace.run_bd(["dep", "remove", &bd_a_id, &bd_b_id], "rm_a_b");
let br_list_after = workspace.run_br(["dep", "list", &br_a_id, "--json"], "list_after");
let bd_list_after = workspace.run_bd(["dep", "list", &bd_a_id, "--json"], "list_after");
let br_after: Value = serde_json::from_str(&extract_json_payload(&br_list_after.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_after: Value = serde_json::from_str(&extract_json_payload(&bd_list_after.stdout))
.unwrap_or(Value::Array(vec![]));
assert_eq!(
br_after.as_array().map(|a| a.len()).unwrap_or(0),
1,
"br should have 1 dep left"
);
assert_eq!(
bd_after.as_array().map(|a| a.len()).unwrap_or(0),
1,
"bd should have 1 dep left"
);
info!("conformance_dep_remove_preserves_other_deps passed");
}
#[test]
fn conformance_dep_list_basic_expanded() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_list_basic_expanded test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_parent = workspace.run_br(["create", "List Parent", "--json"], "create_parent");
let bd_parent = workspace.run_bd(["create", "List Parent", "--json"], "create_parent");
let br_child = workspace.run_br(["create", "List Child", "--json"], "create_child");
let bd_child = workspace.run_bd(["create", "List Child", "--json"], "create_child");
let br_parent_id = extract_issue_id(&extract_json_payload(&br_parent.stdout));
let bd_parent_id = extract_issue_id(&extract_json_payload(&bd_parent.stdout));
let br_child_id = extract_issue_id(&extract_json_payload(&br_child.stdout));
let bd_child_id = extract_issue_id(&extract_json_payload(&bd_child.stdout));
workspace.run_br(["dep", "add", &br_child_id, &br_parent_id], "add_dep");
workspace.run_bd(["dep", "add", &bd_child_id, &bd_parent_id], "add_dep");
let br_list = workspace.run_br(["dep", "list", &br_child_id, "--json"], "list");
let bd_list = workspace.run_bd(["dep", "list", &bd_child_id, "--json"], "list");
assert!(br_list.status.success(), "br dep list failed");
assert!(bd_list.status.success(), "bd dep list failed");
let br_deps: Value = serde_json::from_str(&extract_json_payload(&br_list.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_deps: Value = serde_json::from_str(&extract_json_payload(&bd_list.stdout))
.unwrap_or(Value::Array(vec![]));
assert_eq!(
br_deps.as_array().map(|a| a.len()).unwrap_or(0),
bd_deps.as_array().map(|a| a.len()).unwrap_or(0),
"dep list counts should match"
);
info!("conformance_dep_list_basic_expanded passed");
}
#[test]
fn conformance_dep_list_empty() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_list_empty test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_issue = workspace.run_br(["create", "No deps issue", "--json"], "create");
let bd_issue = workspace.run_bd(["create", "No deps issue", "--json"], "create");
let br_id = extract_issue_id(&extract_json_payload(&br_issue.stdout));
let bd_id = extract_issue_id(&extract_json_payload(&bd_issue.stdout));
let br_list = workspace.run_br(["dep", "list", &br_id, "--json"], "list_empty");
let bd_list = workspace.run_bd(["dep", "list", &bd_id, "--json"], "list_empty");
assert!(br_list.status.success(), "br dep list failed");
assert!(bd_list.status.success(), "bd dep list failed");
let br_deps: Value = serde_json::from_str(&extract_json_payload(&br_list.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_deps: Value = serde_json::from_str(&extract_json_payload(&bd_list.stdout))
.unwrap_or(Value::Array(vec![]));
assert_eq!(
br_deps.as_array().map(|a| a.len()).unwrap_or(0),
0,
"br should have 0 deps"
);
assert_eq!(
bd_deps.as_array().map(|a| a.len()).unwrap_or(0),
0,
"bd should have 0 deps"
);
info!("conformance_dep_list_empty passed");
}
#[test]
fn conformance_dep_list_by_type() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_list_by_type test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_main = workspace.run_br(["create", "Main issue", "--json"], "create_main");
let bd_main = workspace.run_bd(["create", "Main issue", "--json"], "create_main");
let br_blocks = workspace.run_br(["create", "Blocks target", "--json"], "create_blocks");
let bd_blocks = workspace.run_bd(["create", "Blocks target", "--json"], "create_blocks");
let br_related = workspace.run_br(["create", "Related target", "--json"], "create_related");
let bd_related = workspace.run_bd(["create", "Related target", "--json"], "create_related");
let br_main_id = extract_issue_id(&extract_json_payload(&br_main.stdout));
let bd_main_id = extract_issue_id(&extract_json_payload(&bd_main.stdout));
let br_blocks_id = extract_issue_id(&extract_json_payload(&br_blocks.stdout));
let bd_blocks_id = extract_issue_id(&extract_json_payload(&bd_blocks.stdout));
let br_related_id = extract_issue_id(&extract_json_payload(&br_related.stdout));
let bd_related_id = extract_issue_id(&extract_json_payload(&bd_related.stdout));
workspace.run_br(
["dep", "add", &br_main_id, &br_blocks_id, "-t", "blocks"],
"add_blocks",
);
workspace.run_bd(
["dep", "add", &bd_main_id, &bd_blocks_id, "-t", "blocks"],
"add_blocks",
);
workspace.run_br(
["dep", "add", &br_main_id, &br_related_id, "-t", "related"],
"add_related",
);
workspace.run_bd(
["dep", "add", &bd_main_id, &bd_related_id, "-t", "related"],
"add_related",
);
let br_list = workspace.run_br(
["dep", "list", &br_main_id, "-t", "blocks", "--json"],
"list_blocks",
);
let bd_list = workspace.run_bd(
["dep", "list", &bd_main_id, "-t", "blocks", "--json"],
"list_blocks",
);
let br_deps: Value = serde_json::from_str(&extract_json_payload(&br_list.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_deps: Value = serde_json::from_str(&extract_json_payload(&bd_list.stdout))
.unwrap_or(Value::Array(vec![]));
let br_len = br_deps.as_array().map(|a| a.len()).unwrap_or(0);
let bd_len = bd_deps.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_len, bd_len,
"filtered dep counts should match: br={}, bd={}",
br_len, bd_len
);
info!("conformance_dep_list_by_type passed");
}
#[test]
fn conformance_dep_list_json_structure() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_list_json_structure test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "Struct A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "Struct A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "Struct B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "Struct B", "--json"], "create_b");
let br_a_id = extract_issue_id(&extract_json_payload(&br_a.stdout));
let bd_a_id = extract_issue_id(&extract_json_payload(&bd_a.stdout));
let br_b_id = extract_issue_id(&extract_json_payload(&br_b.stdout));
let bd_b_id = extract_issue_id(&extract_json_payload(&bd_b.stdout));
workspace.run_br(["dep", "add", &br_a_id, &br_b_id], "add_dep");
workspace.run_bd(["dep", "add", &bd_a_id, &bd_b_id], "add_dep");
let br_list = workspace.run_br(["dep", "list", &br_a_id, "--json"], "list");
let bd_list = workspace.run_bd(["dep", "list", &bd_a_id, "--json"], "list");
let br_deps: Value = serde_json::from_str(&extract_json_payload(&br_list.stdout))
.expect("br should produce valid JSON");
let bd_deps: Value = serde_json::from_str(&extract_json_payload(&bd_list.stdout))
.expect("bd should produce valid JSON");
assert!(br_deps.is_array(), "br dep list should be an array");
assert!(bd_deps.is_array(), "bd dep list should be an array");
if let Some(br_arr) = br_deps.as_array() {
if let Some(first) = br_arr.first() {
let has_issue_id = first.get("issue_id").is_some();
let has_depends_on = first.get("depends_on_id").is_some();
let has_type = first.get("type").is_some();
assert!(
has_issue_id || has_depends_on,
"br dep list items should have id fields"
);
assert!(has_type, "br dep list items should have type field");
}
}
info!("conformance_dep_list_json_structure passed");
}
#[test]
fn conformance_dep_tree_basic() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_tree_basic test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_root = workspace.run_br(["create", "Tree Root", "--json"], "create_root");
let bd_root = workspace.run_bd(["create", "Tree Root", "--json"], "create_root");
let br_child = workspace.run_br(["create", "Tree Child", "--json"], "create_child");
let bd_child = workspace.run_bd(["create", "Tree Child", "--json"], "create_child");
let br_root_id = extract_issue_id(&extract_json_payload(&br_root.stdout));
let bd_root_id = extract_issue_id(&extract_json_payload(&bd_root.stdout));
let br_child_id = extract_issue_id(&extract_json_payload(&br_child.stdout));
let bd_child_id = extract_issue_id(&extract_json_payload(&bd_child.stdout));
workspace.run_br(["dep", "add", &br_child_id, &br_root_id], "add_dep");
workspace.run_bd(["dep", "add", &bd_child_id, &bd_root_id], "add_dep");
let br_tree = workspace.run_br(["dep", "tree", &br_root_id], "tree");
let bd_tree = workspace.run_bd(["dep", "tree", &bd_root_id], "tree");
assert!(
br_tree.status.success(),
"br dep tree failed: {}",
br_tree.stderr
);
assert!(
bd_tree.status.success(),
"bd dep tree failed: {}",
bd_tree.stderr
);
assert!(
!br_tree.stdout.trim().is_empty(),
"br tree should have output"
);
assert!(
!bd_tree.stdout.trim().is_empty(),
"bd tree should have output"
);
info!("conformance_dep_tree_basic passed");
}
#[test]
fn conformance_dep_tree_deep() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_tree_deep test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "Deep A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "Deep A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "Deep B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "Deep B", "--json"], "create_b");
let br_c = workspace.run_br(["create", "Deep C", "--json"], "create_c");
let bd_c = workspace.run_bd(["create", "Deep C", "--json"], "create_c");
let br_d = workspace.run_br(["create", "Deep D", "--json"], "create_d");
let bd_d = workspace.run_bd(["create", "Deep D", "--json"], "create_d");
let br_a_id = extract_issue_id(&extract_json_payload(&br_a.stdout));
let bd_a_id = extract_issue_id(&extract_json_payload(&bd_a.stdout));
let br_b_id = extract_issue_id(&extract_json_payload(&br_b.stdout));
let bd_b_id = extract_issue_id(&extract_json_payload(&bd_b.stdout));
let br_c_id = extract_issue_id(&extract_json_payload(&br_c.stdout));
let bd_c_id = extract_issue_id(&extract_json_payload(&bd_c.stdout));
let br_d_id = extract_issue_id(&extract_json_payload(&br_d.stdout));
let bd_d_id = extract_issue_id(&extract_json_payload(&bd_d.stdout));
workspace.run_br(["dep", "add", &br_b_id, &br_a_id], "add_b_a");
workspace.run_bd(["dep", "add", &bd_b_id, &bd_a_id], "add_b_a");
workspace.run_br(["dep", "add", &br_c_id, &br_b_id], "add_c_b");
workspace.run_bd(["dep", "add", &bd_c_id, &bd_b_id], "add_c_b");
workspace.run_br(["dep", "add", &br_d_id, &br_c_id], "add_d_c");
workspace.run_bd(["dep", "add", &bd_d_id, &bd_c_id], "add_d_c");
let br_tree = workspace.run_br(["dep", "tree", &br_a_id], "tree");
let bd_tree = workspace.run_bd(["dep", "tree", &bd_a_id], "tree");
assert!(br_tree.status.success(), "br dep tree failed");
assert!(bd_tree.status.success(), "bd dep tree failed");
info!("conformance_dep_tree_deep passed");
}
#[test]
fn conformance_dep_tree_empty() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_tree_empty test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_issue = workspace.run_br(["create", "Tree empty", "--json"], "create");
let bd_issue = workspace.run_bd(["create", "Tree empty", "--json"], "create");
let br_id = extract_issue_id(&extract_json_payload(&br_issue.stdout));
let bd_id = extract_issue_id(&extract_json_payload(&bd_issue.stdout));
let br_tree = workspace.run_br(["dep", "tree", &br_id], "tree");
let bd_tree = workspace.run_bd(["dep", "tree", &bd_id], "tree");
assert!(br_tree.status.success(), "br dep tree failed");
assert!(bd_tree.status.success(), "bd dep tree failed");
info!("conformance_dep_tree_empty passed");
}
#[test]
fn conformance_dep_tree_json() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_tree_json test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_root = workspace.run_br(["create", "JSON Tree Root", "--json"], "create_root");
let bd_root = workspace.run_bd(["create", "JSON Tree Root", "--json"], "create_root");
let br_child = workspace.run_br(["create", "JSON Tree Child", "--json"], "create_child");
let bd_child = workspace.run_bd(["create", "JSON Tree Child", "--json"], "create_child");
let br_root_id = extract_issue_id(&extract_json_payload(&br_root.stdout));
let bd_root_id = extract_issue_id(&extract_json_payload(&bd_root.stdout));
let br_child_id = extract_issue_id(&extract_json_payload(&br_child.stdout));
let bd_child_id = extract_issue_id(&extract_json_payload(&bd_child.stdout));
workspace.run_br(["dep", "add", &br_child_id, &br_root_id], "add_dep");
workspace.run_bd(["dep", "add", &bd_child_id, &bd_root_id], "add_dep");
let br_tree = workspace.run_br(["dep", "tree", &br_root_id, "--json"], "tree_json");
let bd_tree = workspace.run_bd(["dep", "tree", &bd_root_id, "--json"], "tree_json");
let br_success = br_tree.status.success();
let bd_success = bd_tree.status.success();
assert_eq!(
br_success, bd_success,
"br and bd should both succeed or fail for tree --json"
);
if br_success {
let br_json = extract_json_payload(&br_tree.stdout);
let bd_json = extract_json_payload(&bd_tree.stdout);
let br_val: Result<Value, _> = serde_json::from_str(&br_json);
let bd_val: Result<Value, _> = serde_json::from_str(&bd_json);
assert!(br_val.is_ok(), "br tree JSON should be valid");
assert!(bd_val.is_ok(), "bd tree JSON should be valid");
}
info!("conformance_dep_tree_json passed");
}
#[test]
fn conformance_dep_cycles_none() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_cycles_none test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "NoCycle A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "NoCycle A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "NoCycle B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "NoCycle B", "--json"], "create_b");
let br_a_id = extract_issue_id(&extract_json_payload(&br_a.stdout));
let bd_a_id = extract_issue_id(&extract_json_payload(&bd_a.stdout));
let br_b_id = extract_issue_id(&extract_json_payload(&br_b.stdout));
let bd_b_id = extract_issue_id(&extract_json_payload(&bd_b.stdout));
workspace.run_br(
["dep", "add", &br_a_id, &br_b_id, "-t", "related"],
"add_dep",
);
workspace.run_bd(
["dep", "add", &bd_a_id, &bd_b_id, "-t", "related"],
"add_dep",
);
let br_cycles = workspace.run_br(["dep", "cycles", "--json"], "cycles");
let bd_cycles = workspace.run_bd(["dep", "cycles", "--json"], "cycles");
assert!(br_cycles.status.success(), "br dep cycles failed");
assert!(bd_cycles.status.success(), "bd dep cycles failed");
let br_json = extract_json_payload(&br_cycles.stdout);
let bd_json = extract_json_payload(&bd_cycles.stdout);
let br_val: Value = serde_json::from_str(&br_json).unwrap_or(Value::Null);
let bd_val: Value = serde_json::from_str(&bd_json).unwrap_or(Value::Null);
let br_count = br_val["count"].as_u64().unwrap_or(0);
let bd_count = bd_val["count"].as_u64().unwrap_or(0);
assert_eq!(br_count, 0, "br should find no cycles");
assert_eq!(bd_count, 0, "bd should find no cycles");
info!("conformance_dep_cycles_none passed");
}
#[test]
fn conformance_dep_cycles_simple() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_cycles_simple test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "SimpleCycle A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "SimpleCycle A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "SimpleCycle B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "SimpleCycle B", "--json"], "create_b");
let br_a_id = extract_issue_id(&extract_json_payload(&br_a.stdout));
let bd_a_id = extract_issue_id(&extract_json_payload(&bd_a.stdout));
let br_b_id = extract_issue_id(&extract_json_payload(&br_b.stdout));
let bd_b_id = extract_issue_id(&extract_json_payload(&bd_b.stdout));
workspace.run_br(
["dep", "add", &br_a_id, &br_b_id, "-t", "related"],
"add_a_b",
);
workspace.run_bd(
["dep", "add", &bd_a_id, &bd_b_id, "-t", "related"],
"add_a_b",
);
workspace.run_br(
["dep", "add", &br_b_id, &br_a_id, "-t", "related"],
"add_b_a",
);
workspace.run_bd(
["dep", "add", &bd_b_id, &bd_a_id, "-t", "related"],
"add_b_a",
);
let br_cycles = workspace.run_br(["dep", "cycles", "--json"], "cycles");
let bd_cycles = workspace.run_bd(["dep", "cycles", "--json"], "cycles");
assert!(br_cycles.status.success(), "br dep cycles failed");
assert!(bd_cycles.status.success(), "bd dep cycles failed");
let br_json = extract_json_payload(&br_cycles.stdout);
let bd_json = extract_json_payload(&bd_cycles.stdout);
let br_val: Value = serde_json::from_str(&br_json).unwrap_or(Value::Null);
let bd_val: Value = serde_json::from_str(&bd_json).unwrap_or(Value::Null);
let br_count = br_val["count"].as_u64().unwrap_or(0);
let bd_count = bd_val["count"].as_u64().unwrap_or(0);
info!(
"Cycle detection: br={}, bd={} (known difference: br detects in all types)",
br_count, bd_count
);
assert!(
br_count >= 1,
"br should detect cycle in 'related' dependencies"
);
info!("conformance_dep_cycles_simple passed");
}
#[test]
fn conformance_dep_cycles_complex() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_cycles_complex test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "ComplexCycle A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "ComplexCycle A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "ComplexCycle B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "ComplexCycle B", "--json"], "create_b");
let br_c = workspace.run_br(["create", "ComplexCycle C", "--json"], "create_c");
let bd_c = workspace.run_bd(["create", "ComplexCycle C", "--json"], "create_c");
let br_a_id = extract_issue_id(&extract_json_payload(&br_a.stdout));
let bd_a_id = extract_issue_id(&extract_json_payload(&bd_a.stdout));
let br_b_id = extract_issue_id(&extract_json_payload(&br_b.stdout));
let bd_b_id = extract_issue_id(&extract_json_payload(&bd_b.stdout));
let br_c_id = extract_issue_id(&extract_json_payload(&br_c.stdout));
let bd_c_id = extract_issue_id(&extract_json_payload(&bd_c.stdout));
workspace.run_br(
["dep", "add", &br_a_id, &br_b_id, "-t", "related"],
"add_a_b",
);
workspace.run_bd(
["dep", "add", &bd_a_id, &bd_b_id, "-t", "related"],
"add_a_b",
);
workspace.run_br(
["dep", "add", &br_b_id, &br_c_id, "-t", "related"],
"add_b_c",
);
workspace.run_bd(
["dep", "add", &bd_b_id, &bd_c_id, "-t", "related"],
"add_b_c",
);
workspace.run_br(
["dep", "add", &br_c_id, &br_a_id, "-t", "related"],
"add_c_a",
);
workspace.run_bd(
["dep", "add", &bd_c_id, &bd_a_id, "-t", "related"],
"add_c_a",
);
let br_cycles = workspace.run_br(["dep", "cycles", "--json"], "cycles");
let bd_cycles = workspace.run_bd(["dep", "cycles", "--json"], "cycles");
assert!(br_cycles.status.success(), "br dep cycles failed");
assert!(bd_cycles.status.success(), "bd dep cycles failed");
let br_json = extract_json_payload(&br_cycles.stdout);
let bd_json = extract_json_payload(&bd_cycles.stdout);
let br_val: Value = serde_json::from_str(&br_json).unwrap_or(Value::Null);
let bd_val: Value = serde_json::from_str(&bd_json).unwrap_or(Value::Null);
let br_count = br_val["count"].as_u64().unwrap_or(0);
let bd_count = bd_val["count"].as_u64().unwrap_or(0);
info!(
"Complex cycle detection: br={}, bd={} (known difference: br detects in all types)",
br_count, bd_count
);
assert!(
br_count >= 1,
"br should detect cycle in 'related' dependencies"
);
info!("conformance_dep_cycles_complex passed");
}
#[test]
fn conformance_dep_cycles_json() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_dep_cycles_json test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_cycles = workspace.run_br(["dep", "cycles", "--json"], "cycles");
let bd_cycles = workspace.run_bd(["dep", "cycles", "--json"], "cycles");
assert!(br_cycles.status.success(), "br dep cycles --json failed");
assert!(bd_cycles.status.success(), "bd dep cycles --json failed");
let br_json = extract_json_payload(&br_cycles.stdout);
let bd_json = extract_json_payload(&bd_cycles.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("br should produce valid JSON");
let bd_val: Value = serde_json::from_str(&bd_json).unwrap_or(Value::Null);
assert!(
br_val.get("cycles").is_some() || br_val.get("count").is_some(),
"br cycles JSON should have cycles or count field"
);
info!(
"JSON structure - br: cycles={}, count={} | bd: cycles={}, count={}",
br_val.get("cycles").is_some(),
br_val.get("count").is_some(),
bd_val.get("cycles").is_some(),
bd_val.get("count").is_some()
);
info!("conformance_dep_cycles_json passed");
}
#[test]
fn conformance_stats_empty() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_stats_empty test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_stats = workspace.run_br(["stats", "--no-activity", "--json"], "stats_empty");
let bd_stats = workspace.run_bd(["stats", "--no-activity", "--json"], "stats_empty");
assert!(
br_stats.status.success(),
"br stats on empty workspace failed: {}",
br_stats.stderr
);
assert!(
bd_stats.status.success(),
"bd stats on empty workspace failed: {}",
bd_stats.stderr
);
let br_json = extract_json_payload(&br_stats.stdout);
let bd_json = extract_json_payload(&bd_stats.stdout);
log_timings("stats_empty", &br_stats, &bd_stats);
compare_json(
&br_json,
&bd_json,
&CompareMode::FieldsExcluded(vec!["average_lead_time_hours".to_string()]),
)
.expect("JSON mismatch");
info!("conformance_stats_empty passed");
}
#[test]
fn conformance_stats_mixed() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_stats_mixed test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let _br_create1 = workspace.run_br(["create", "Open issue", "--json"], "create1");
let _bd_create1 = workspace.run_bd(["create", "Open issue", "--json"], "create1");
let br_create2 = workspace.run_br(["create", "Will close", "--json"], "create2");
let bd_create2 = workspace.run_bd(["create", "Will close", "--json"], "create2");
let br_id = extract_issue_id(&extract_json_payload(&br_create2.stdout));
let bd_id = extract_issue_id(&extract_json_payload(&bd_create2.stdout));
workspace.run_br(["close", &br_id], "close");
workspace.run_bd(["close", &bd_id], "close");
let br_stats = workspace.run_br(["stats", "--no-activity", "--json"], "stats");
let bd_stats = workspace.run_bd(["stats", "--no-activity", "--json"], "stats");
assert!(
br_stats.status.success(),
"br stats failed: {}",
br_stats.stderr
);
assert!(
bd_stats.status.success(),
"bd stats failed: {}",
bd_stats.stderr
);
let br_json = extract_json_payload(&br_stats.stdout);
let bd_json = extract_json_payload(&bd_stats.stdout);
log_timings("stats_mixed", &br_stats, &bd_stats);
compare_json(
&br_json,
&bd_json,
&CompareMode::FieldsExcluded(vec!["average_lead_time_hours".to_string()]),
)
.expect("JSON mismatch");
info!("conformance_stats_mixed passed");
}
#[test]
fn conformance_stats_with_deps() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_stats_with_deps test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "Issue A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "Issue A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "Issue B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "Issue B", "--json"], "create_b");
let br_a_id = extract_issue_id(&extract_json_payload(&br_a.stdout));
let bd_a_id = extract_issue_id(&extract_json_payload(&bd_a.stdout));
let br_b_id = extract_issue_id(&extract_json_payload(&br_b.stdout));
let bd_b_id = extract_issue_id(&extract_json_payload(&bd_b.stdout));
workspace.run_br(["dep", "add", &br_a_id, &br_b_id], "add_dep");
workspace.run_bd(["dep", "add", &bd_a_id, &bd_b_id], "add_dep");
let br_stats = workspace.run_br(["stats", "--no-activity", "--json"], "stats");
let bd_stats = workspace.run_bd(["stats", "--no-activity", "--json"], "stats");
assert!(br_stats.status.success(), "br stats failed");
assert!(bd_stats.status.success(), "bd stats failed");
let br_json = extract_json_payload(&br_stats.stdout);
let bd_json = extract_json_payload(&bd_stats.stdout);
log_timings("stats_with_deps", &br_stats, &bd_stats);
compare_json(
&br_json,
&bd_json,
&CompareMode::FieldsExcluded(vec!["average_lead_time_hours".to_string()]),
)
.expect("JSON mismatch");
info!("conformance_stats_with_deps passed");
}
#[test]
fn conformance_stats_json_shape() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_stats_json_shape test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Test issue"], "create");
workspace.run_bd(["create", "Test issue"], "create");
let br_stats = workspace.run_br(["stats", "--no-activity", "--json"], "stats");
let bd_stats = workspace.run_bd(["stats", "--no-activity", "--json"], "stats");
assert!(br_stats.status.success(), "br stats failed");
assert!(bd_stats.status.success(), "bd stats failed");
let br_json = extract_json_payload(&br_stats.stdout);
let bd_json = extract_json_payload(&bd_stats.stdout);
log_timings("stats_all_fields", &br_stats, &bd_stats);
let br_val: Value = serde_json::from_str(&br_json).expect("br json");
let bd_val: Value = serde_json::from_str(&bd_json).expect("bd json");
let excluded = vec!["average_lead_time_hours".to_string()];
let br_filtered = filter_fields(&br_val, &excluded);
let bd_filtered = filter_fields(&bd_val, &excluded);
assert!(
structure_matches(&br_filtered, &bd_filtered),
"stats JSON structure mismatch"
);
log_timings("stats_json_shape", &br_stats, &bd_stats);
info!("conformance_stats_json_shape passed");
}
#[test]
fn conformance_count_by_status() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_count_by_status test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Open 1"], "create1");
workspace.run_bd(["create", "Open 1"], "create1");
let br_create2 = workspace.run_br(["create", "Will close", "--json"], "create2");
let bd_create2 = workspace.run_bd(["create", "Will close", "--json"], "create2");
let br_id = extract_issue_id(&extract_json_payload(&br_create2.stdout));
let bd_id = extract_issue_id(&extract_json_payload(&bd_create2.stdout));
workspace.run_br(["close", &br_id], "close");
workspace.run_bd(["close", &bd_id], "close");
let br_count = workspace.run_br(
["count", "--by", "status", "--json", "--include-closed"],
"count",
);
let bd_count = workspace.run_bd(["count", "--by-status", "--json"], "count");
assert!(
br_count.status.success(),
"br count failed: {}",
br_count.stderr
);
assert!(
bd_count.status.success(),
"bd count failed: {}",
bd_count.stderr
);
let br_json = extract_json_payload(&br_count.stdout);
let bd_json = extract_json_payload(&bd_count.stdout);
log_timings("count_by_status", &br_count, &bd_count);
compare_json(&br_json, &bd_json, &CompareMode::NormalizedJson).expect("JSON mismatch");
info!("conformance_count_by_status passed");
}
#[test]
fn conformance_count_by_type() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_count_by_type test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Task 1", "--type", "task"], "create1");
workspace.run_bd(["create", "Task 1", "--type", "task"], "create1");
workspace.run_br(["create", "Bug 1", "--type", "bug"], "create2");
workspace.run_bd(["create", "Bug 1", "--type", "bug"], "create2");
workspace.run_br(["create", "Feature 1", "--type", "feature"], "create3");
workspace.run_bd(["create", "Feature 1", "--type", "feature"], "create3");
let br_count = workspace.run_br(["count", "--by", "type", "--json"], "count");
let bd_count = workspace.run_bd(["count", "--by-type", "--json"], "count");
assert!(
br_count.status.success(),
"br count failed: {}",
br_count.stderr
);
assert!(
bd_count.status.success(),
"bd count failed: {}",
bd_count.stderr
);
let br_json = extract_json_payload(&br_count.stdout);
let bd_json = extract_json_payload(&bd_count.stdout);
log_timings("count_by_type", &br_count, &bd_count);
compare_json(&br_json, &bd_json, &CompareMode::NormalizedJson).expect("JSON mismatch");
info!("conformance_count_by_type passed");
}
#[test]
fn conformance_count_by_priority() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_count_by_priority test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "P0 issue", "-p", "0"], "create1");
workspace.run_bd(["create", "P0 issue", "-p", "0"], "create1");
workspace.run_br(["create", "P1 issue", "-p", "1"], "create2");
workspace.run_bd(["create", "P1 issue", "-p", "1"], "create2");
workspace.run_br(["create", "P2 issue", "-p", "2"], "create3");
workspace.run_bd(["create", "P2 issue", "-p", "2"], "create3");
let br_count = workspace.run_br(["count", "--by", "priority", "--json"], "count");
let bd_count = workspace.run_bd(["count", "--by-priority", "--json"], "count");
assert!(
br_count.status.success(),
"br count failed: {}",
br_count.stderr
);
assert!(
bd_count.status.success(),
"bd count failed: {}",
bd_count.stderr
);
let br_json = extract_json_payload(&br_count.stdout);
let bd_json = extract_json_payload(&bd_count.stdout);
log_timings("count_by_priority", &br_count, &bd_count);
compare_json(&br_json, &bd_json, &CompareMode::NormalizedJson).expect("JSON mismatch");
info!("conformance_count_by_priority passed");
}
#[test]
fn conformance_count_by_assignee() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_count_by_assignee test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(
["create", "Assigned to Alice", "--assignee", "alice"],
"create1",
);
workspace.run_bd(
["create", "Assigned to Alice", "--assignee", "alice"],
"create1",
);
workspace.run_br(
["create", "Assigned to Bob", "--assignee", "bob"],
"create2",
);
workspace.run_bd(
["create", "Assigned to Bob", "--assignee", "bob"],
"create2",
);
workspace.run_br(["create", "Unassigned"], "create3");
workspace.run_bd(["create", "Unassigned"], "create3");
let br_count = workspace.run_br(["count", "--by", "assignee", "--json"], "count");
let bd_count = workspace.run_bd(["count", "--by-assignee", "--json"], "count");
assert!(
br_count.status.success(),
"br count failed: {}",
br_count.stderr
);
assert!(
bd_count.status.success(),
"bd count failed: {}",
bd_count.stderr
);
let br_json = extract_json_payload(&br_count.stdout);
let bd_json = extract_json_payload(&bd_count.stdout);
log_timings("count_by_assignee", &br_count, &bd_count);
compare_json(&br_json, &bd_json, &CompareMode::NormalizedJson).expect("JSON mismatch");
info!("conformance_count_by_assignee passed");
}
#[test]
fn conformance_count_json_shape() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_count_json_shape test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Test"], "create");
workspace.run_bd(["create", "Test"], "create");
let br_count = workspace.run_br(["count", "--json"], "count");
let bd_count = workspace.run_bd(["count", "--json"], "count");
assert!(br_count.status.success(), "br count failed");
assert!(bd_count.status.success(), "bd count failed");
let br_json = extract_json_payload(&br_count.stdout);
let bd_json = extract_json_payload(&bd_count.stdout);
log_timings("count_json_shape", &br_count, &bd_count);
compare_json(&br_json, &bd_json, &CompareMode::StructureOnly).expect("JSON mismatch");
info!("conformance_count_json_shape passed");
}
#[test]
fn conformance_count_empty() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_count_empty test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_count = workspace.run_br(["count", "--json"], "count");
let bd_count = workspace.run_bd(["count", "--json"], "count");
assert!(
br_count.status.success(),
"br count failed: {}",
br_count.stderr
);
assert!(
bd_count.status.success(),
"bd count failed: {}",
bd_count.stderr
);
let br_json = extract_json_payload(&br_count.stdout);
let bd_json = extract_json_payload(&bd_count.stdout);
log_timings("count_empty", &br_count, &bd_count);
compare_json(&br_json, &bd_json, &CompareMode::ExactJson).expect("JSON mismatch");
info!("conformance_count_empty passed");
}
#[test]
fn conformance_stale_default() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_stale_default test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Fresh issue"], "create");
workspace.run_bd(["create", "Fresh issue"], "create");
let br_stale = workspace.run_br(["stale", "--json"], "stale");
let bd_stale = workspace.run_bd(["stale", "--json"], "stale");
assert!(
br_stale.status.success(),
"br stale failed: {}",
br_stale.stderr
);
assert!(
bd_stale.status.success(),
"bd stale failed: {}",
bd_stale.stderr
);
let br_json = extract_json_payload(&br_stale.stdout);
let bd_json = extract_json_payload(&bd_stale.stdout);
log_timings("stale_default", &br_stale, &bd_stale);
compare_json(&br_json, &bd_json, &CompareMode::NormalizedJson).expect("JSON mismatch");
info!("conformance_stale_default passed");
}
#[test]
#[ignore]
fn conformance_stale_custom_days() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_stale_custom_days test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Test issue"], "create");
workspace.run_bd(["create", "Test issue"], "create");
let br_stale = workspace.run_br(["stale", "--days", "0", "--json"], "stale");
let bd_stale = workspace.run_bd(["stale", "--days", "0", "--json"], "stale");
assert!(
br_stale.status.success(),
"br stale --days 0 failed: {}",
br_stale.stderr
);
assert!(
bd_stale.status.success(),
"bd stale --days 0 failed: {}",
bd_stale.stderr
);
let br_json = extract_json_payload(&br_stale.stdout);
let bd_json = extract_json_payload(&bd_stale.stdout);
log_timings("stale_custom_days", &br_stale, &bd_stale);
compare_json(&br_json, &bd_json, &CompareMode::NormalizedJson).expect("JSON mismatch");
info!("conformance_stale_custom_days passed");
}
#[test]
fn conformance_stale_empty() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_stale_empty test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_stale = workspace.run_br(["stale", "--json"], "stale");
let bd_stale = workspace.run_bd(["stale", "--json"], "stale");
assert!(
br_stale.status.success(),
"br stale failed: {}",
br_stale.stderr
);
assert!(
bd_stale.status.success(),
"bd stale failed: {}",
bd_stale.stderr
);
let br_json = extract_json_payload(&br_stale.stdout);
let bd_json = extract_json_payload(&bd_stale.stdout);
log_timings("stale_empty", &br_stale, &bd_stale);
compare_json(&br_json, &bd_json, &CompareMode::ExactJson).expect("JSON mismatch");
info!("conformance_stale_empty passed");
}
#[test]
fn conformance_stale_excludes_closed() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_stale_excludes_closed test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Will close", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Will close", "--json"], "create");
let br_id = extract_issue_id(&extract_json_payload(&br_create.stdout));
let bd_id = extract_issue_id(&extract_json_payload(&bd_create.stdout));
workspace.run_br(["close", &br_id], "close");
workspace.run_bd(["close", &bd_id], "close");
let br_stale = workspace.run_br(["stale", "--days", "0", "--json"], "stale");
let bd_stale = workspace.run_bd(["stale", "--days", "0", "--json"], "stale");
assert!(br_stale.status.success(), "br stale failed");
assert!(bd_stale.status.success(), "bd stale failed");
let br_json = extract_json_payload(&br_stale.stdout);
let bd_json = extract_json_payload(&bd_stale.stdout);
log_timings("stale_excludes_closed", &br_stale, &bd_stale);
compare_json(&br_json, &bd_json, &CompareMode::NormalizedJson).expect("JSON mismatch");
let br_ids: HashSet<String> = serde_json::from_str::<Value>(&br_json)
.ok()
.and_then(|v| {
v.as_array().map(|arr| {
arr.iter()
.filter_map(|item| item.get("id").and_then(|id| id.as_str()))
.map(|id| id.to_string())
.collect()
})
})
.unwrap_or_default();
let bd_ids: HashSet<String> = serde_json::from_str::<Value>(&bd_json)
.ok()
.and_then(|v| {
v.as_array().map(|arr| {
arr.iter()
.filter_map(|item| item.get("id").and_then(|id| id.as_str()))
.map(|id| id.to_string())
.collect()
})
})
.unwrap_or_default();
assert!(!br_ids.contains(&br_id), "br stale includes closed issue");
assert!(!bd_ids.contains(&bd_id), "bd stale includes closed issue");
info!("conformance_stale_excludes_closed passed");
}
#[test]
fn conformance_stale_json_shape() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_stale_json_shape test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Test"], "create");
workspace.run_bd(["create", "Test"], "create");
let br_stale = workspace.run_br(["stale", "--json"], "stale");
let bd_stale = workspace.run_bd(["stale", "--json"], "stale");
assert!(br_stale.status.success(), "br stale failed");
assert!(bd_stale.status.success(), "bd stale failed");
let br_json = extract_json_payload(&br_stale.stdout);
let bd_json = extract_json_payload(&bd_stale.stdout);
log_timings("stale_json_shape", &br_stale, &bd_stale);
compare_json(&br_json, &bd_json, &CompareMode::StructureOnly).expect("JSON mismatch");
info!("conformance_stale_json_shape passed");
}
#[test]
#[ignore]
fn conformance_doctor_healthy() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_doctor_healthy test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_doctor = workspace.run_br(["doctor", "--json"], "doctor");
let bd_doctor = workspace.run_bd(["doctor", "--json"], "doctor");
assert!(
br_doctor.status.success(),
"br doctor failed on healthy workspace: {}",
br_doctor.stderr
);
assert!(
bd_doctor.status.success(),
"bd doctor failed on healthy workspace: {}",
bd_doctor.stderr
);
let br_json = extract_json_payload(&br_doctor.stdout);
let bd_json = extract_json_payload(&bd_doctor.stdout);
let br_checks = extract_checks_len(&br_json);
let bd_checks = extract_checks_len(&bd_json);
assert!(br_checks > 0, "br doctor should emit checks");
assert!(bd_checks > 0, "bd doctor should emit checks");
log_timings("doctor_healthy", &br_doctor, &bd_doctor);
info!("conformance_doctor_healthy passed");
}
#[test]
#[ignore]
fn conformance_doctor_json_shape() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_doctor_json_shape test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_doctor = workspace.run_br(["doctor", "--json"], "doctor");
let bd_doctor = workspace.run_bd(["doctor", "--json"], "doctor");
assert!(br_doctor.status.success(), "br doctor failed");
assert!(bd_doctor.status.success(), "bd doctor failed");
let br_json = extract_json_payload(&br_doctor.stdout);
let bd_json = extract_json_payload(&bd_doctor.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("br doctor json");
let bd_val: Value = serde_json::from_str(&bd_json).expect("bd doctor json");
let br_checks = br_val
.get("checks")
.and_then(|checks| checks.as_array())
.cloned()
.unwrap_or_default();
let bd_checks = bd_val
.get("checks")
.and_then(|checks| checks.as_array())
.cloned()
.unwrap_or_default();
assert!(
br_checks
.iter()
.all(|c| c.get("name").is_some() && c.get("status").is_some()),
"br doctor checks missing name/status"
);
assert!(
bd_checks
.iter()
.all(|c| c.get("name").is_some() && c.get("status").is_some()),
"bd doctor checks missing name/status"
);
log_timings("doctor_json_shape", &br_doctor, &bd_doctor);
info!("conformance_doctor_json_shape passed");
}
#[test]
#[ignore]
fn conformance_doctor_with_issues() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_doctor_with_issues test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "Issue A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "Issue A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "Issue B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "Issue B", "--json"], "create_b");
let br_a_id = extract_issue_id(&extract_json_payload(&br_a.stdout));
let bd_a_id = extract_issue_id(&extract_json_payload(&bd_a.stdout));
let br_b_id = extract_issue_id(&extract_json_payload(&br_b.stdout));
let bd_b_id = extract_issue_id(&extract_json_payload(&bd_b.stdout));
workspace.run_br(["dep", "add", &br_a_id, &br_b_id], "add_dep");
workspace.run_bd(["dep", "add", &bd_a_id, &bd_b_id], "add_dep");
let br_doctor = workspace.run_br(["doctor", "--json"], "doctor");
let bd_doctor = workspace.run_bd(["doctor", "--json"], "doctor");
assert!(br_doctor.status.success(), "br doctor failed with issues");
assert!(bd_doctor.status.success(), "bd doctor failed with issues");
let br_json = extract_json_payload(&br_doctor.stdout);
let bd_json = extract_json_payload(&bd_doctor.stdout);
assert!(
extract_checks_len(&br_json) > 0,
"br doctor should emit checks"
);
assert!(
extract_checks_len(&bd_json) > 0,
"bd doctor should emit checks"
);
log_timings("doctor_with_issues", &br_doctor, &bd_doctor);
info!("conformance_doctor_with_issues passed");
}
#[test]
#[ignore = "bd returns extra config object with compaction settings not implemented in br"]
fn conformance_info_json_parity() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_info_json_parity test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_info = workspace.run_br(["info", "--json"], "info");
let bd_info = workspace.run_bd(["info", "--json"], "info");
assert!(
br_info.status.success(),
"br info failed: {}",
br_info.stderr
);
assert!(
bd_info.status.success(),
"bd info failed: {}",
bd_info.stderr
);
let br_json = extract_json_payload(&br_info.stdout);
let bd_json = extract_json_payload(&bd_info.stdout);
let mut br_val: Value = serde_json::from_str(&br_json).expect("br info json");
let mut bd_val: Value = serde_json::from_str(&bd_json).expect("bd info json");
normalize_path_fields(&mut br_val, &workspace.br_root);
normalize_path_fields(&mut bd_val, &workspace.bd_root);
let excluded = vec![
"beads_dir".to_string(),
"db_size".to_string(),
"jsonl_path".to_string(),
"jsonl_size".to_string(),
"daemon_detail".to_string(),
"daemon_fallback_reason".to_string(),
];
let br_filtered = filter_fields(&br_val, &excluded);
let bd_filtered = filter_fields(&bd_val, &excluded);
assert_eq!(
br_filtered,
bd_filtered,
"info JSON mismatch after normalization\nbr: {}\nbd: {}",
serde_json::to_string_pretty(&br_filtered).unwrap_or_default(),
serde_json::to_string_pretty(&bd_filtered).unwrap_or_default()
);
log_timings("info_json_parity", &br_info, &bd_info);
info!("conformance_info_json_parity passed");
}
#[test]
#[ignore = "bd returns extra prefix field not implemented in br"]
fn conformance_where_json_parity() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_where_json_parity test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_where = workspace.run_br(["where", "--json"], "where");
let bd_where = workspace.run_bd(["where", "--json"], "where");
assert!(
br_where.status.success(),
"br where failed: {}",
br_where.stderr
);
assert!(
bd_where.status.success(),
"bd where failed: {}",
bd_where.stderr
);
let br_json = extract_json_payload(&br_where.stdout);
let bd_json = extract_json_payload(&bd_where.stdout);
let mut br_val: Value = serde_json::from_str(&br_json).expect("br where json");
let mut bd_val: Value = serde_json::from_str(&bd_json).expect("bd where json");
normalize_path_fields(&mut br_val, &workspace.br_root);
normalize_path_fields(&mut bd_val, &workspace.bd_root);
let excluded = vec!["jsonl_path".to_string()];
let br_filtered = filter_fields(&br_val, &excluded);
let bd_filtered = filter_fields(&bd_val, &excluded);
assert_eq!(
br_filtered,
bd_filtered,
"where JSON mismatch after normalization\nbr: {}\nbd: {}",
serde_json::to_string_pretty(&br_filtered).unwrap_or_default(),
serde_json::to_string_pretty(&bd_filtered).unwrap_or_default()
);
log_timings("where_json_parity", &br_where, &bd_where);
info!("conformance_where_json_parity passed");
}
#[test]
fn conformance_version_text() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_version_text test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_version = workspace.run_br(["version"], "version");
let bd_version = workspace.run_bd(["version"], "version");
assert!(
br_version.status.success(),
"br version failed: {}",
br_version.stderr
);
assert!(
bd_version.status.success(),
"bd version failed: {}",
bd_version.stderr
);
assert!(
!br_version.stdout.trim().is_empty(),
"br version should produce output"
);
assert!(
!bd_version.stdout.trim().is_empty(),
"bd version should produce output"
);
log_timings("version_text", &br_version, &bd_version);
info!("conformance_version_text passed");
}
#[test]
fn conformance_version_json() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_version_json test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_version = workspace.run_br(["version", "--json"], "version");
let bd_version = workspace.run_bd(["version", "--json"], "version");
assert!(
br_version.status.success(),
"br version --json failed: {}",
br_version.stderr
);
assert!(
bd_version.status.success(),
"bd version --json failed: {}",
bd_version.stderr
);
let br_json = extract_json_payload(&br_version.stdout);
let bd_json = extract_json_payload(&bd_version.stdout);
let br_val: Result<Value, _> = serde_json::from_str(&br_json);
let bd_val: Result<Value, _> = serde_json::from_str(&bd_json);
assert!(br_val.is_ok(), "br version should produce valid JSON");
assert!(bd_val.is_ok(), "bd version should produce valid JSON");
log_timings("version_json", &br_version, &bd_version);
info!("conformance_version_json passed");
}
#[test]
fn conformance_version_fields() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_version_fields test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_version = workspace.run_br(["version", "--json"], "version");
let bd_version = workspace.run_bd(["version", "--json"], "version");
assert!(br_version.status.success(), "br version failed");
assert!(bd_version.status.success(), "bd version failed");
let br_json = extract_json_payload(&br_version.stdout);
let bd_json = extract_json_payload(&bd_version.stdout);
let br_val: Value = serde_json::from_str(&br_json).unwrap_or(Value::Null);
let bd_val: Value = serde_json::from_str(&bd_json).unwrap_or(Value::Null);
let br_has_version = br_val.get("version").is_some();
let bd_has_version = bd_val.get("version").is_some();
let br_has_build = br_val.get("build").is_some();
let bd_has_build = bd_val.get("build").is_some();
assert!(br_has_version, "br version should have version field");
assert!(bd_has_version, "bd version should have version field");
assert!(br_has_build, "br version should have build field");
assert!(bd_has_build, "bd version should have build field");
log_timings("version_fields", &br_version, &bd_version);
info!("conformance_version_fields passed");
}
#[test]
fn conformance_config_list() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_config_list test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_config = workspace.run_br(["config", "list", "--json"], "config_list");
let bd_config = workspace.run_bd(["config", "list", "--json"], "config_list");
assert!(
br_config.status.success(),
"br config list failed: {}",
br_config.stderr
);
assert!(
bd_config.status.success(),
"bd config list failed: {}",
bd_config.stderr
);
let br_json = extract_json_payload(&br_config.stdout);
let bd_json = extract_json_payload(&bd_config.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("br config json");
let bd_val: Value = serde_json::from_str(&bd_json).expect("bd config json");
let br_prefix = br_val.get("issue_prefix").and_then(|v| v.as_str());
let bd_prefix = bd_val.get("issue_prefix").and_then(|v| v.as_str());
assert!(br_prefix.is_some(), "br config list missing issue_prefix");
assert!(bd_prefix.is_some(), "bd config list missing issue_prefix");
log_timings("config_list", &br_config, &bd_config);
info!("conformance_config_list passed");
}
#[test]
#[ignore]
fn conformance_config_get() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_config_get test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_set = workspace.run_br(["config", "set", "issue_prefix=cfg_get"], "config_set");
let bd_set = workspace.run_bd(["config", "set", "issue_prefix", "cfg_get"], "config_set");
assert!(
br_set.status.success(),
"br config set failed: {}",
br_set.stderr
);
assert!(
bd_set.status.success(),
"bd config set failed: {}",
bd_set.stderr
);
let br_get = workspace.run_br(["config", "get", "issue_prefix", "--json"], "config_get");
let bd_get = workspace.run_bd(["config", "get", "issue_prefix", "--json"], "config_get");
assert!(
br_get.status.success(),
"br config get failed: {}",
br_get.stderr
);
assert!(
bd_get.status.success(),
"bd config get failed: {}",
bd_get.stderr
);
let br_json = extract_json_payload(&br_get.stdout);
let bd_json = extract_json_payload(&bd_get.stdout);
log_timings("config_get", &br_get, &bd_get);
compare_json(&br_json, &bd_json, &CompareMode::ExactJson).expect("JSON mismatch");
info!("conformance_config_get passed");
}
#[test]
fn conformance_config_set() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_config_set test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_set = workspace.run_br(["config", "set", "issue_prefix=cfg_set"], "config_set");
let bd_set = workspace.run_bd(["config", "set", "issue_prefix", "cfg_set"], "config_set");
assert!(
br_set.status.success(),
"br config set failed: {}",
br_set.stderr
);
assert!(
bd_set.status.success(),
"bd config set failed: {}",
bd_set.stderr
);
log_timings("config_set", &br_set, &bd_set);
info!("conformance_config_set passed");
}
#[test]
#[ignore]
fn conformance_config_get_after_set() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_config_get_after_set test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_set = workspace.run_br(["config", "set", "issue_prefix=cfg_after"], "config_set");
let bd_set = workspace.run_bd(["config", "set", "issue_prefix", "cfg_after"], "config_set");
assert!(
br_set.status.success(),
"br config set failed: {}",
br_set.stderr
);
assert!(
bd_set.status.success(),
"bd config set failed: {}",
bd_set.stderr
);
let br_get = workspace.run_br(["config", "get", "issue_prefix", "--json"], "config_get");
let bd_get = workspace.run_bd(["config", "get", "issue_prefix", "--json"], "config_get");
assert!(
br_get.status.success(),
"br config get failed: {}",
br_get.stderr
);
assert!(
bd_get.status.success(),
"bd config get failed: {}",
bd_get.stderr
);
let br_json = extract_json_payload(&br_get.stdout);
let bd_json = extract_json_payload(&bd_get.stdout);
log_timings("config_get_after_set", &br_get, &bd_get);
compare_json(&br_json, &bd_json, &CompareMode::ExactJson).expect("JSON mismatch");
info!("conformance_config_get_after_set passed");
}
#[test]
fn conformance_config_json_shape() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_config_json_shape test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_config = workspace.run_br(["config", "list", "--json"], "config");
let bd_config = workspace.run_bd(["config", "list", "--json"], "config");
assert!(br_config.status.success(), "br config list failed");
assert!(bd_config.status.success(), "bd config list failed");
let br_json = extract_json_payload(&br_config.stdout);
let bd_json = extract_json_payload(&bd_config.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("br config json");
let bd_val: Value = serde_json::from_str(&bd_json).expect("bd config json");
assert!(br_val.is_object(), "br config list should be object");
assert!(bd_val.is_object(), "bd config list should be object");
log_timings("config_json_shape", &br_config, &bd_config);
info!("conformance_config_json_shape passed");
}
#[test]
fn conformance_config_defaults() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_config_defaults test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_config = workspace.run_br(["config", "list", "--json"], "config_defaults");
let bd_config = workspace.run_bd(["config", "list", "--json"], "config_defaults");
assert!(br_config.status.success(), "br config list failed");
assert!(bd_config.status.success(), "bd config list failed");
let br_json = extract_json_payload(&br_config.stdout);
let bd_json = extract_json_payload(&bd_config.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("br config json");
let bd_val: Value = serde_json::from_str(&bd_json).expect("bd config json");
let br_prefix = br_val
.get("issue_prefix")
.and_then(|v| v.as_str())
.unwrap_or("");
let bd_prefix = bd_val
.get("issue_prefix")
.and_then(|v| v.as_str())
.unwrap_or("");
assert!(
!br_prefix.is_empty(),
"br config defaults should include issue_prefix"
);
assert!(
!bd_prefix.is_empty(),
"bd config defaults should include issue_prefix"
);
log_timings("config_defaults", &br_config, &bd_config);
info!("conformance_config_defaults passed");
}
#[test]
#[ignore]
fn conformance_config_invalid_key() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_config_invalid_key test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_config = workspace.run_br(
["config", "get", "nonexistent.key.that.does.not.exist"],
"config_invalid",
);
let bd_config = workspace.run_bd(
["config", "get", "nonexistent.key.that.does.not.exist"],
"config_invalid",
);
assert_eq!(
br_config.status.success(),
bd_config.status.success(),
"br/bd config invalid key exit mismatch"
);
assert!(
!br_config.status.success(),
"config get should fail for invalid key"
);
log_timings("config_invalid_key", &br_config, &bd_config);
info!("conformance_config_invalid_key passed");
}
#[test]
fn conformance_close_already_closed() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_close_already_closed test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "To close twice", "--json"], "create");
let bd_create = workspace.run_bd(["create", "To close twice", "--json"], "create");
let br_id = extract_id_from_json(&br_create.stdout);
let bd_id = extract_id_from_json(&bd_create.stdout);
workspace.run_br(["close", &br_id], "close1");
workspace.run_bd(["close", &bd_id], "close1");
let br_close2 = workspace.run_br(["close", &br_id], "close2");
let bd_close2 = workspace.run_bd(["close", &bd_id], "close2");
info!(
"br double close: success={}, bd double close: success={}",
br_close2.status.success(),
bd_close2.status.success()
);
info!("conformance_close_already_closed passed");
}
#[test]
fn conformance_close_sets_closed_at() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_close_sets_closed_at test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Track close time", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Track close time", "--json"], "create");
let br_id = extract_id_from_json(&br_create.stdout);
let bd_id = extract_id_from_json(&bd_create.stdout);
workspace.run_br(["close", &br_id, "--json"], "close");
workspace.run_bd(["close", &bd_id, "--json"], "close");
let br_show = workspace.run_br(["show", &br_id, "--json"], "show_closed");
let bd_show = workspace.run_bd(["show", &bd_id, "--json"], "show_closed");
let br_issue = issue_value_from_show_output(&br_show, "br show closed issue");
let bd_issue = issue_value_from_show_output(&bd_show, "bd show closed issue");
let br_has_closed_at = br_issue.get("closed_at").is_some() && !br_issue["closed_at"].is_null();
let bd_has_closed_at = bd_issue.get("closed_at").is_some() && !bd_issue["closed_at"].is_null();
info!(
"br has closed_at: {}, bd has closed_at: {}",
br_has_closed_at, bd_has_closed_at
);
assert!(
br_has_closed_at,
"br closed issue missing closed_at: {br_issue:?}"
);
assert!(
bd_has_closed_at,
"bd closed issue missing closed_at: {bd_issue:?}"
);
info!("conformance_close_sets_closed_at passed");
}
#[test]
fn conformance_close_blocked_issue() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_close_blocked_issue test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "Blocker", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "Blocker", "--json"], "create_a");
let br_b = workspace.run_br(["create", "Blocked", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "Blocked", "--json"], "create_b");
let br_a_id = extract_id_from_json(&br_a.stdout);
let bd_a_id = extract_id_from_json(&bd_a.stdout);
let br_b_id = extract_id_from_json(&br_b.stdout);
let bd_b_id = extract_id_from_json(&bd_b.stdout);
workspace.run_br(["dep", "add", &br_b_id, &br_a_id], "dep_add");
workspace.run_bd(["dep", "add", &bd_b_id, &bd_a_id], "dep_add");
let br_close = workspace.run_br(["close", &br_b_id], "close_blocked");
let bd_close = workspace.run_bd(["close", &bd_b_id], "close_blocked");
info!(
"br close blocked: success={}, bd close blocked: success={}",
br_close.status.success(),
bd_close.status.success()
);
info!("conformance_close_blocked_issue passed");
}
#[test]
fn conformance_close_updates_dependents() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_close_updates_dependents test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "Blocker A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "Blocker A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "Dependent B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "Dependent B", "--json"], "create_b");
let br_a_id = extract_id_from_json(&br_a.stdout);
let bd_a_id = extract_id_from_json(&bd_a.stdout);
let br_b_id = extract_id_from_json(&br_b.stdout);
let bd_b_id = extract_id_from_json(&bd_b.stdout);
workspace.run_br(["dep", "add", &br_b_id, &br_a_id], "dep_add");
workspace.run_bd(["dep", "add", &bd_b_id, &bd_a_id], "dep_add");
let br_blocked = workspace.run_br(["blocked", "--json"], "blocked_before");
let bd_blocked = workspace.run_bd(["blocked", "--json"], "blocked_before");
assert!(br_blocked.status.success(), "br blocked failed");
assert!(bd_blocked.status.success(), "bd blocked failed");
workspace.run_br(["close", &br_a_id], "close_blocker");
workspace.run_bd(["close", &bd_a_id], "close_blocker");
let br_ready = workspace.run_br(["ready", "--json"], "ready_after");
let bd_ready = workspace.run_bd(["ready", "--json"], "ready_after");
assert!(br_ready.status.success(), "br ready failed");
assert!(bd_ready.status.success(), "bd ready failed");
info!("conformance_close_updates_dependents passed");
}
#[test]
fn conformance_close_preserves_fields() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_close_preserves_fields test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(
[
"create",
"Feature to close",
"--type",
"feature",
"--priority",
"1",
"--assignee",
"dev",
"--json",
],
"create",
);
let bd_create = workspace.run_bd(
[
"create",
"Feature to close",
"--type",
"feature",
"--priority",
"1",
"--assignee",
"dev",
"--json",
],
"create",
);
let br_id = extract_id_from_json(&br_create.stdout);
let bd_id = extract_id_from_json(&bd_create.stdout);
workspace.run_br(
["close", &br_id, "--reason", "Completed successfully"],
"close",
);
workspace.run_bd(
["close", &bd_id, "--reason", "Completed successfully"],
"close",
);
let br_show = workspace.run_br(["show", &br_id, "--json"], "show_closed");
let bd_show = workspace.run_bd(["show", &bd_id, "--json"], "show_closed");
let br_json = extract_json_payload(&br_show.stdout);
let bd_json = extract_json_payload(&bd_show.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse br");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse bd");
let br_issue = if br_val.is_array() {
&br_val[0]
} else {
&br_val
};
let bd_issue = if bd_val.is_array() {
&bd_val[0]
} else {
&bd_val
};
assert_eq!(
br_issue["priority"].as_i64(),
Some(1),
"br priority changed after close"
);
assert_eq!(
bd_issue["priority"].as_i64(),
Some(1),
"bd priority changed after close"
);
info!("conformance_close_preserves_fields passed");
}
#[test]
fn conformance_reopen_clears_closed_at() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_reopen_clears_closed_at test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "To reopen", "--json"], "create");
let bd_create = workspace.run_bd(["create", "To reopen", "--json"], "create");
let br_id = extract_id_from_json(&br_create.stdout);
let bd_id = extract_id_from_json(&bd_create.stdout);
workspace.run_br(["close", &br_id], "close");
workspace.run_bd(["close", &bd_id], "close");
workspace.run_br(["reopen", &br_id], "reopen");
workspace.run_bd(["reopen", &bd_id], "reopen");
let br_show = workspace.run_br(["show", &br_id, "--json"], "show_reopened");
let bd_show = workspace.run_bd(["show", &bd_id, "--json"], "show_reopened");
let br_issue = issue_value_from_show_output(&br_show, "br show reopened issue");
let bd_issue = issue_value_from_show_output(&bd_show, "bd show reopened issue");
let br_closed_at = br_issue.get("closed_at");
let bd_closed_at = bd_issue.get("closed_at");
info!(
"br closed_at after reopen: {:?}, bd closed_at after reopen: {:?}",
br_closed_at, bd_closed_at
);
assert!(
br_closed_at.is_none_or(Value::is_null),
"br reopened issue should clear closed_at: {br_issue:?}"
);
assert!(
bd_closed_at.is_none_or(Value::is_null),
"bd reopened issue should clear closed_at: {bd_issue:?}"
);
info!("conformance_reopen_clears_closed_at passed");
}
#[test]
fn conformance_reopen_preserves_fields() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_reopen_preserves_fields test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(
[
"create",
"Reopen test",
"--type",
"bug",
"--priority",
"0",
"--json",
],
"create",
);
let bd_create = workspace.run_bd(
[
"create",
"Reopen test",
"--type",
"bug",
"--priority",
"0",
"--json",
],
"create",
);
let br_id = extract_id_from_json(&br_create.stdout);
let bd_id = extract_id_from_json(&bd_create.stdout);
workspace.run_br(["close", &br_id, "--reason", "Done"], "close");
workspace.run_bd(["close", &bd_id, "--reason", "Done"], "close");
workspace.run_br(["reopen", &br_id], "reopen");
workspace.run_bd(["reopen", &bd_id], "reopen");
let br_show = workspace.run_br(["show", &br_id, "--json"], "show");
let bd_show = workspace.run_bd(["show", &bd_id, "--json"], "show");
let br_json = extract_json_payload(&br_show.stdout);
let bd_json = extract_json_payload(&bd_show.stdout);
let br_val: Value = serde_json::from_str(&br_json).expect("parse br");
let bd_val: Value = serde_json::from_str(&bd_json).expect("parse bd");
let br_issue = if br_val.is_array() {
&br_val[0]
} else {
&br_val
};
let bd_issue = if bd_val.is_array() {
&bd_val[0]
} else {
&bd_val
};
assert_eq!(
br_issue["priority"].as_i64(),
Some(0),
"br priority changed after reopen"
);
assert_eq!(
bd_issue["priority"].as_i64(),
Some(0),
"bd priority changed after reopen"
);
info!("conformance_reopen_preserves_fields passed");
}
#[test]
fn conformance_reopen_never_closed_error() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_reopen_never_closed_error test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Never closed", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Never closed", "--json"], "create");
let br_id = extract_id_from_json(&br_create.stdout);
let bd_id = extract_id_from_json(&bd_create.stdout);
let br_reopen = workspace.run_br(["reopen", &br_id], "reopen_not_closed");
let bd_reopen = workspace.run_bd(["reopen", &bd_id], "reopen_not_closed");
info!(
"br reopen never closed: success={}, bd reopen never closed: success={}",
br_reopen.status.success(),
bd_reopen.status.success()
);
info!("conformance_reopen_never_closed_error passed");
}
#[test]
fn conformance_reopen_tombstone_error() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_reopen_tombstone_error test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "To tombstone", "--json"], "create");
let bd_create = workspace.run_bd(["create", "To tombstone", "--json"], "create");
let br_id = extract_id_from_json(&br_create.stdout);
let bd_id = extract_id_from_json(&bd_create.stdout);
workspace.run_br(["delete", &br_id], "delete");
workspace.run_bd(["delete", &bd_id], "delete");
let br_reopen = workspace.run_br(["reopen", &br_id], "reopen_tombstone");
let bd_reopen = workspace.run_bd(["reopen", &bd_id], "reopen_tombstone");
info!(
"br reopen tombstone: success={}, bd reopen tombstone: success={}",
br_reopen.status.success(),
bd_reopen.status.success()
);
info!("conformance_reopen_tombstone_error passed");
}
#[test]
fn conformance_epic_status_empty() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_epic_status_empty test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_out = workspace.run_br(["epic", "status", "--json"], "epic_status_empty");
let bd_out = workspace.run_bd(["epic", "status", "--json"], "epic_status_empty");
info!(
"br epic status empty: success={}, bd epic status empty: success={}",
br_out.status.success(),
bd_out.status.success()
);
assert!(
br_out.status.success(),
"br epic status failed: {}",
br_out.stderr
);
assert!(
bd_out.status.success(),
"bd epic status failed: {}",
bd_out.stderr
);
info!("conformance_epic_status_empty passed");
}
#[test]
fn conformance_epic_status_with_epic() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_epic_status_with_epic test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(
["create", "Test epic", "--type", "epic", "--json"],
"create_epic",
);
let bd_create = workspace.run_bd(
["create", "Test epic", "--type", "epic", "--json"],
"create_epic",
);
assert!(
br_create.status.success(),
"br create epic failed: {}",
br_create.stderr
);
assert!(
bd_create.status.success(),
"bd create epic failed: {}",
bd_create.stderr
);
let br_out = workspace.run_br(["epic", "status", "--json"], "epic_status");
let bd_out = workspace.run_bd(["epic", "status", "--json"], "epic_status");
assert!(
br_out.status.success(),
"br epic status failed: {}",
br_out.stderr
);
assert!(
bd_out.status.success(),
"bd epic status failed: {}",
bd_out.stderr
);
info!(
"br epic status result length: {}, bd epic status result length: {}",
br_out.stdout.len(),
bd_out.stdout.len()
);
info!("conformance_epic_status_with_epic passed");
}
#[test]
fn conformance_epic_status_with_children() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_epic_status_with_children test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_epic = workspace.run_br(
["create", "Parent epic", "--type", "epic", "--json"],
"create_epic",
);
let bd_epic = workspace.run_bd(
["create", "Parent epic", "--type", "epic", "--json"],
"create_epic",
);
let br_epic_id = extract_id_from_json(&br_epic.stdout);
let bd_epic_id = extract_id_from_json(&bd_epic.stdout);
let br_child = workspace.run_br(
["create", "Child task", "--type", "task", "--json"],
"create_child",
);
let bd_child = workspace.run_bd(
["create", "Child task", "--type", "task", "--json"],
"create_child",
);
let br_child_id = extract_id_from_json(&br_child.stdout);
let bd_child_id = extract_id_from_json(&bd_child.stdout);
workspace.run_br(
[
"dep",
"add",
&br_child_id,
&br_epic_id,
"--type",
"parent-child",
],
"add_parent-child",
);
workspace.run_bd(
[
"dep",
"add",
&bd_child_id,
&bd_epic_id,
"--type",
"parent-child",
],
"add_parent-child",
);
let br_out = workspace.run_br(["epic", "status", "--json"], "epic_status_children");
let bd_out = workspace.run_bd(["epic", "status", "--json"], "epic_status_children");
assert!(
br_out.status.success(),
"br epic status failed: {}",
br_out.stderr
);
assert!(
bd_out.status.success(),
"bd epic status failed: {}",
bd_out.stderr
);
info!("conformance_epic_status_with_children passed");
}
#[test]
fn conformance_epic_close_eligible_open_children() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_epic_close_eligible_open_children test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_epic = workspace.run_br(
["create", "Epic with open child", "--type", "epic", "--json"],
"create_epic",
);
let bd_epic = workspace.run_bd(
["create", "Epic with open child", "--type", "epic", "--json"],
"create_epic",
);
let br_epic_id = extract_id_from_json(&br_epic.stdout);
let bd_epic_id = extract_id_from_json(&bd_epic.stdout);
let br_child = workspace.run_br(["create", "Open child", "--json"], "create_child");
let bd_child = workspace.run_bd(["create", "Open child", "--json"], "create_child");
let br_child_id = extract_id_from_json(&br_child.stdout);
let bd_child_id = extract_id_from_json(&bd_child.stdout);
workspace.run_br(
[
"dep",
"add",
&br_child_id,
&br_epic_id,
"--type",
"parent-child",
],
"add_parent-child",
);
workspace.run_bd(
[
"dep",
"add",
&bd_child_id,
&bd_epic_id,
"--type",
"parent-child",
],
"add_parent-child",
);
let br_out = workspace.run_br(["epic", "close-eligible", "--json"], "close_eligible");
let bd_out = workspace.run_bd(["epic", "close-eligible", "--json"], "close_eligible");
info!(
"br close-eligible result: success={}, bd close-eligible result: success={}",
br_out.status.success(),
bd_out.status.success()
);
let br_show = workspace.run_br(["show", &br_epic_id, "--json"], "show_epic");
let bd_show = workspace.run_bd(["show", &bd_epic_id, "--json"], "show_epic");
let br_json = extract_json_payload(&br_show.stdout);
let bd_json = extract_json_payload(&bd_show.stdout);
let br_status: Value = serde_json::from_str(&br_json).expect("parse br json");
let bd_status: Value = serde_json::from_str(&bd_json).expect("parse bd json");
assert_eq!(
br_status[0].get("status").and_then(|v| v.as_str()),
Some("open"),
"br epic should still be open"
);
assert_eq!(
bd_status[0].get("status").and_then(|v| v.as_str()),
Some("open"),
"bd epic should still be open"
);
info!("conformance_epic_close_eligible_open_children passed");
}
#[test]
#[ignore = "br parent-child dependency blocks children; bd does not"]
fn conformance_epic_close_eligible_all_closed() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_epic_close_eligible_all_closed test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_epic = workspace.run_br(
["create", "Epic all closed", "--type", "epic", "--json"],
"create_epic",
);
let bd_epic = workspace.run_bd(
["create", "Epic all closed", "--type", "epic", "--json"],
"create_epic",
);
let br_epic_id = extract_id_from_json(&br_epic.stdout);
let bd_epic_id = extract_id_from_json(&bd_epic.stdout);
let br_child = workspace.run_br(["create", "Child to close", "--json"], "create_child");
let bd_child = workspace.run_bd(["create", "Child to close", "--json"], "create_child");
let br_child_id = extract_id_from_json(&br_child.stdout);
let bd_child_id = extract_id_from_json(&bd_child.stdout);
workspace.run_br(
[
"dep",
"add",
&br_child_id,
&br_epic_id,
"--type",
"parent-child",
],
"add_parent-child",
);
workspace.run_bd(
[
"dep",
"add",
&bd_child_id,
&bd_epic_id,
"--type",
"parent-child",
],
"add_parent-child",
);
workspace.run_br(["close", &br_child_id], "close_child");
workspace.run_bd(["close", &bd_child_id], "close_child");
let br_out = workspace.run_br(["epic", "close-eligible", "--json"], "close_eligible");
let bd_out = workspace.run_bd(["epic", "close-eligible", "--json"], "close_eligible");
info!(
"br close-eligible all closed: success={}, bd close-eligible all closed: success={}",
br_out.status.success(),
bd_out.status.success()
);
let br_show = workspace.run_br(["show", &br_epic_id, "--json"], "show_epic_after");
let bd_show = workspace.run_bd(["show", &bd_epic_id, "--json"], "show_epic_after");
let br_json = extract_json_payload(&br_show.stdout);
let bd_json = extract_json_payload(&bd_show.stdout);
let br_status: Value = serde_json::from_str(&br_json).expect("parse br json");
let bd_status: Value = serde_json::from_str(&bd_json).expect("parse bd json");
assert_eq!(
br_status[0].get("status").and_then(|v| v.as_str()),
Some("closed"),
"br epic should be closed"
);
assert_eq!(
bd_status[0].get("status").and_then(|v| v.as_str()),
Some("closed"),
"bd epic should be closed"
);
info!("conformance_epic_close_eligible_all_closed passed");
}
#[test]
fn conformance_epic_status_eligible_only() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_epic_status_eligible_only test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let _br_epic1 = workspace.run_br(
["create", "Epic no children", "--type", "epic", "--json"],
"create_epic1",
);
let _bd_epic1 = workspace.run_bd(
["create", "Epic no children", "--type", "epic", "--json"],
"create_epic1",
);
let br_epic2 = workspace.run_br(
["create", "Epic with open child", "--type", "epic", "--json"],
"create_epic2",
);
let bd_epic2 = workspace.run_bd(
["create", "Epic with open child", "--type", "epic", "--json"],
"create_epic2",
);
let br_epic2_id = extract_id_from_json(&br_epic2.stdout);
let bd_epic2_id = extract_id_from_json(&bd_epic2.stdout);
let br_child = workspace.run_br(["create", "Open child", "--json"], "create_child");
let bd_child = workspace.run_bd(["create", "Open child", "--json"], "create_child");
let br_child_id = extract_id_from_json(&br_child.stdout);
let bd_child_id = extract_id_from_json(&bd_child.stdout);
workspace.run_br(
[
"dep",
"add",
&br_child_id,
&br_epic2_id,
"--type",
"parent-child",
],
"add_parent-child",
);
workspace.run_bd(
[
"dep",
"add",
&bd_child_id,
&bd_epic2_id,
"--type",
"parent-child",
],
"add_parent-child",
);
let br_out = workspace.run_br(
["epic", "status", "--eligible-only", "--json"],
"epic_eligible_only",
);
let bd_out = workspace.run_bd(
["epic", "status", "--eligible-only", "--json"],
"epic_eligible_only",
);
assert!(
br_out.status.success(),
"br epic status failed: {}",
br_out.stderr
);
assert!(
bd_out.status.success(),
"bd epic status failed: {}",
bd_out.stderr
);
info!(
"br eligible-only result: {}, bd eligible-only result: {}",
br_out.stdout.trim(),
bd_out.stdout.trim()
);
info!("conformance_epic_status_eligible_only passed");
}
#[test]
fn conformance_epic_status_json_shape() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_epic_status_json_shape test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(
["create", "JSON shape test epic", "--type", "epic", "--json"],
"create_epic",
);
workspace.run_bd(
["create", "JSON shape test epic", "--type", "epic", "--json"],
"create_epic",
);
let br_out = workspace.run_br(["epic", "status", "--json"], "epic_status_json");
let bd_out = workspace.run_bd(["epic", "status", "--json"], "epic_status_json");
assert!(
br_out.status.success(),
"br epic status failed: {}",
br_out.stderr
);
assert!(
bd_out.status.success(),
"bd epic status failed: {}",
bd_out.stderr
);
let br_json = extract_json_payload(&br_out.stdout);
let bd_json = extract_json_payload(&bd_out.stdout);
let result = compare_json(&br_json, &bd_json, &CompareMode::StructureOnly);
if let Err(e) = &result {
info!("Structure comparison note (may differ): {}", e);
}
info!("conformance_epic_status_json_shape passed");
}
#[test]
fn conformance_epic_nested() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_epic_nested test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_parent = workspace.run_br(
["create", "Parent epic", "--type", "epic", "--json"],
"create_parent",
);
let bd_parent = workspace.run_bd(
["create", "Parent epic", "--type", "epic", "--json"],
"create_parent",
);
let br_parent_id = extract_id_from_json(&br_parent.stdout);
let bd_parent_id = extract_id_from_json(&bd_parent.stdout);
let br_child = workspace.run_br(
["create", "Child epic", "--type", "epic", "--json"],
"create_child_epic",
);
let bd_child = workspace.run_bd(
["create", "Child epic", "--type", "epic", "--json"],
"create_child_epic",
);
let br_child_id = extract_id_from_json(&br_child.stdout);
let bd_child_id = extract_id_from_json(&bd_child.stdout);
workspace.run_br(
[
"dep",
"add",
&br_child_id,
&br_parent_id,
"--type",
"parent-child",
],
"add_nested",
);
workspace.run_bd(
[
"dep",
"add",
&bd_child_id,
&bd_parent_id,
"--type",
"parent-child",
],
"add_nested",
);
let br_out = workspace.run_br(["epic", "status", "--json"], "epic_nested_status");
let bd_out = workspace.run_bd(["epic", "status", "--json"], "epic_nested_status");
assert!(
br_out.status.success(),
"br epic status failed: {}",
br_out.stderr
);
assert!(
bd_out.status.success(),
"bd epic status failed: {}",
bd_out.stderr
);
info!("conformance_epic_nested passed");
}
#[test]
fn conformance_graph_no_deps() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_graph_no_deps test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "No deps issue", "--json"], "create");
let bd_create = workspace.run_bd(["create", "No deps issue", "--json"], "create");
let br_id = extract_id_from_json(&br_create.stdout);
let bd_id = extract_id_from_json(&bd_create.stdout);
let br_out = workspace.run_br(["graph", &br_id, "--json"], "graph_no_deps");
let bd_out = workspace.run_bd(["graph", &bd_id, "--json"], "graph_no_deps");
info!(
"br graph no deps: success={}, bd graph no deps: success={}",
br_out.status.success(),
bd_out.status.success()
);
assert_json_output_mentions(&br_out, &br_id, "br graph no deps");
assert_json_output_mentions(&bd_out, &bd_id, "bd graph no deps");
info!("conformance_graph_no_deps passed");
}
#[test]
fn conformance_graph_simple_dep() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_graph_simple_dep test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "Issue A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "Issue A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "Issue B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "Issue B", "--json"], "create_b");
let br_a_id = extract_id_from_json(&br_a.stdout);
let bd_a_id = extract_id_from_json(&bd_a.stdout);
let br_b_id = extract_id_from_json(&br_b.stdout);
let bd_b_id = extract_id_from_json(&bd_b.stdout);
let br_dep = workspace.run_br(["dep", "add", &br_a_id, &br_b_id], "add_dep");
let bd_dep = workspace.run_bd(["dep", "add", &bd_a_id, &bd_b_id], "add_dep");
assert_command_success(&br_dep, "br graph simple dep setup");
assert_command_success(&bd_dep, "bd graph simple dep setup");
let br_out = workspace.run_br(["graph", &br_a_id, "--json"], "graph_simple");
let bd_out = workspace.run_bd(["graph", &bd_a_id, "--json"], "graph_simple");
info!(
"br graph simple dep: success={}, bd graph simple dep: success={}",
br_out.status.success(),
bd_out.status.success()
);
assert_json_output_mentions(&br_out, &br_a_id, "br graph simple root");
assert_json_output_mentions(&br_out, &br_b_id, "br graph simple dependency");
assert_json_output_mentions(&bd_out, &bd_a_id, "bd graph simple root");
assert_json_output_mentions(&bd_out, &bd_b_id, "bd graph simple dependency");
info!("conformance_graph_simple_dep passed");
}
#[test]
fn conformance_graph_complex_deps() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_graph_complex_deps test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "Issue A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "Issue A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "Issue B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "Issue B", "--json"], "create_b");
let br_c = workspace.run_br(["create", "Issue C", "--json"], "create_c");
let bd_c = workspace.run_bd(["create", "Issue C", "--json"], "create_c");
let br_d = workspace.run_br(["create", "Issue D", "--json"], "create_d");
let bd_d = workspace.run_bd(["create", "Issue D", "--json"], "create_d");
let br_a_id = extract_id_from_json(&br_a.stdout);
let bd_a_id = extract_id_from_json(&bd_a.stdout);
let br_b_id = extract_id_from_json(&br_b.stdout);
let bd_b_id = extract_id_from_json(&bd_b.stdout);
let br_c_id = extract_id_from_json(&br_c.stdout);
let bd_c_id = extract_id_from_json(&bd_c.stdout);
let br_d_id = extract_id_from_json(&br_d.stdout);
let bd_d_id = extract_id_from_json(&bd_d.stdout);
let br_dep_ab = workspace.run_br(["dep", "add", &br_a_id, &br_b_id], "dep_ab");
let bd_dep_ab = workspace.run_bd(["dep", "add", &bd_a_id, &bd_b_id], "dep_ab");
let br_dep_ac = workspace.run_br(["dep", "add", &br_a_id, &br_c_id], "dep_ac");
let bd_dep_ac = workspace.run_bd(["dep", "add", &bd_a_id, &bd_c_id], "dep_ac");
let br_dep_bd = workspace.run_br(["dep", "add", &br_b_id, &br_d_id], "dep_bd");
let bd_dep_bd = workspace.run_bd(["dep", "add", &bd_b_id, &bd_d_id], "dep_bd");
let br_dep_cd = workspace.run_br(["dep", "add", &br_c_id, &br_d_id], "dep_cd");
let bd_dep_cd = workspace.run_bd(["dep", "add", &bd_c_id, &bd_d_id], "dep_cd");
assert_command_success(&br_dep_ab, "br graph complex dep_ab setup");
assert_command_success(&bd_dep_ab, "bd graph complex dep_ab setup");
assert_command_success(&br_dep_ac, "br graph complex dep_ac setup");
assert_command_success(&bd_dep_ac, "bd graph complex dep_ac setup");
assert_command_success(&br_dep_bd, "br graph complex dep_bd setup");
assert_command_success(&bd_dep_bd, "bd graph complex dep_bd setup");
assert_command_success(&br_dep_cd, "br graph complex dep_cd setup");
assert_command_success(&bd_dep_cd, "bd graph complex dep_cd setup");
let br_out = workspace.run_br(["graph", &br_a_id, "--json"], "graph_complex");
let bd_out = workspace.run_bd(["graph", &bd_a_id, "--json"], "graph_complex");
info!(
"br graph complex: success={}, bd graph complex: success={}",
br_out.status.success(),
bd_out.status.success()
);
for expected in [&br_a_id, &br_b_id, &br_c_id, &br_d_id] {
assert_json_output_mentions(&br_out, expected, "br graph complex");
}
for expected in [&bd_a_id, &bd_b_id, &bd_c_id, &bd_d_id] {
assert_json_output_mentions(&bd_out, expected, "bd graph complex");
}
info!("conformance_graph_complex_deps passed");
}
#[test]
fn conformance_graph_all_flag() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_graph_all_flag test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_issue1 = workspace.run_br(["create", "Issue 1", "--json"], "create1");
let bd_issue1 = workspace.run_bd(["create", "Issue 1", "--json"], "create1");
let br_issue2 = workspace.run_br(["create", "Issue 2", "--json"], "create2");
let bd_issue2 = workspace.run_bd(["create", "Issue 2", "--json"], "create2");
let br_issue1_id = extract_id_from_json(&br_issue1.stdout);
let bd_issue1_id = extract_id_from_json(&bd_issue1.stdout);
let br_issue2_id = extract_id_from_json(&br_issue2.stdout);
let bd_issue2_id = extract_id_from_json(&bd_issue2.stdout);
let br_out = workspace.run_br(["graph", "--all", "--json"], "graph_all");
let bd_out = workspace.run_bd(["graph", "--all", "--json"], "graph_all");
info!(
"br graph --all: success={}, bd graph --all: success={}",
br_out.status.success(),
bd_out.status.success()
);
assert_json_output_mentions(&br_out, &br_issue1_id, "br graph all first issue");
assert_json_output_mentions(&br_out, &br_issue2_id, "br graph all second issue");
assert_json_output_mentions(&bd_out, &bd_issue1_id, "bd graph all first issue");
assert_json_output_mentions(&bd_out, &bd_issue2_id, "bd graph all second issue");
info!("conformance_graph_all_flag passed");
}
#[test]
#[ignore = "br-only feature: --compact flag not in bd v0.46.0"]
fn conformance_graph_compact_flag() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_graph_compact_flag test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "Issue A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "Issue A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "Issue B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "Issue B", "--json"], "create_b");
let br_a_id = extract_id_from_json(&br_a.stdout);
let bd_a_id = extract_id_from_json(&bd_a.stdout);
let br_b_id = extract_id_from_json(&br_b.stdout);
let bd_b_id = extract_id_from_json(&bd_b.stdout);
workspace.run_br(["dep", "add", &br_a_id, &br_b_id], "add_dep");
workspace.run_bd(["dep", "add", &bd_a_id, &bd_b_id], "add_dep");
let br_out = workspace.run_br(["graph", &br_a_id, "--compact"], "graph_compact");
let bd_out = workspace.run_bd(["graph", &bd_a_id, "--compact"], "graph_compact");
info!(
"br graph --compact: success={}, bd graph --compact: success={}",
br_out.status.success(),
bd_out.status.success()
);
assert!(
br_out.status.success(),
"br graph --compact failed: {}",
br_out.stderr
);
assert!(
bd_out.status.success(),
"bd graph --compact failed: {}",
bd_out.stderr
);
info!("conformance_graph_compact_flag passed");
}
#[test]
fn conformance_graph_json_shape() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_graph_json_shape test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_a = workspace.run_br(["create", "Issue A", "--json"], "create_a");
let bd_a = workspace.run_bd(["create", "Issue A", "--json"], "create_a");
let br_b = workspace.run_br(["create", "Issue B", "--json"], "create_b");
let bd_b = workspace.run_bd(["create", "Issue B", "--json"], "create_b");
let br_a_id = extract_id_from_json(&br_a.stdout);
let bd_a_id = extract_id_from_json(&bd_a.stdout);
let br_b_id = extract_id_from_json(&br_b.stdout);
let bd_b_id = extract_id_from_json(&bd_b.stdout);
workspace.run_br(["dep", "add", &br_a_id, &br_b_id], "add_dep");
workspace.run_bd(["dep", "add", &bd_a_id, &bd_b_id], "add_dep");
let br_out = workspace.run_br(["graph", &br_a_id, "--json"], "graph_json");
let bd_out = workspace.run_bd(["graph", &bd_a_id, "--json"], "graph_json");
assert!(
br_out.status.success(),
"br graph --json failed: {}",
br_out.stderr
);
assert!(
bd_out.status.success(),
"bd graph --json failed: {}",
bd_out.stderr
);
let br_json = extract_json_payload(&br_out.stdout);
let bd_json = extract_json_payload(&bd_out.stdout);
let result = compare_json(&br_json, &bd_json, &CompareMode::StructureOnly);
if let Err(e) = &result {
info!("Graph JSON structure comparison note (may differ): {}", e);
}
info!("conformance_graph_json_shape passed");
}
#[test]
fn conformance_audit_record_llm_call() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_audit_record_llm_call test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_out = workspace.run_br(
[
"audit",
"record",
"--kind",
"llm_call",
"--model",
"gpt-4",
"--prompt",
"Hello world",
"--response",
"Hi there!",
"--json",
],
"audit_llm_call",
);
let bd_out = workspace.run_bd(
[
"audit",
"record",
"--kind",
"llm_call",
"--model",
"gpt-4",
"--prompt",
"Hello world",
"--response",
"Hi there!",
"--json",
],
"audit_llm_call",
);
info!(
"br audit record llm_call: success={}, bd audit record llm_call: success={}",
br_out.status.success(),
bd_out.status.success()
);
assert!(
br_out.status.success(),
"br audit record failed: {}",
br_out.stderr
);
assert!(
bd_out.status.success(),
"bd audit record failed: {}",
bd_out.stderr
);
info!("conformance_audit_record_llm_call passed");
}
#[test]
fn conformance_audit_record_tool_call() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_audit_record_tool_call test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_out = workspace.run_br(
[
"audit",
"record",
"--kind",
"tool_call",
"--tool-name",
"bash",
"--exit-code",
"0",
"--json",
],
"audit_tool_call",
);
let bd_out = workspace.run_bd(
[
"audit",
"record",
"--kind",
"tool_call",
"--tool-name",
"bash",
"--exit-code",
"0",
"--json",
],
"audit_tool_call",
);
info!(
"br audit record tool_call: success={}, bd audit record tool_call: success={}",
br_out.status.success(),
bd_out.status.success()
);
assert!(
br_out.status.success(),
"br audit record tool_call failed: {}",
br_out.stderr
);
assert!(
bd_out.status.success(),
"bd audit record tool_call failed: {}",
bd_out.stderr
);
info!("conformance_audit_record_tool_call passed");
}
#[test]
fn conformance_audit_record_with_issue() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_audit_record_with_issue test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Test issue for audit", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Test issue for audit", "--json"], "create");
let br_id = extract_id_from_json(&br_create.stdout);
let bd_id = extract_id_from_json(&bd_create.stdout);
let br_out = workspace.run_br(
[
"audit",
"record",
"--kind",
"llm_call",
"--issue-id",
&br_id,
"--model",
"claude",
"--prompt",
"Fix the bug",
"--response",
"Bug fixed",
"--json",
],
"audit_with_issue",
);
let bd_out = workspace.run_bd(
[
"audit",
"record",
"--kind",
"llm_call",
"--issue-id",
&bd_id,
"--model",
"claude",
"--prompt",
"Fix the bug",
"--response",
"Bug fixed",
"--json",
],
"audit_with_issue",
);
info!(
"br audit record with issue: success={}, bd audit record with issue: success={}",
br_out.status.success(),
bd_out.status.success()
);
assert_command_success(&br_out, "br audit record with issue");
assert_command_success(&bd_out, "bd audit record with issue");
let br_record: Value =
serde_json::from_str(&extract_json_payload(&br_out.stdout)).expect("br audit record json");
let bd_record: Value =
serde_json::from_str(&extract_json_payload(&bd_out.stdout)).expect("bd audit record json");
assert!(
br_record["id"].as_str().is_some_and(|id| !id.is_empty()),
"br audit record should return an id: {br_record:?}"
);
assert_eq!(br_record["kind"].as_str(), Some("llm_call"));
assert!(
bd_record["id"].as_str().is_some_and(|id| !id.is_empty()),
"bd audit record should return an id: {bd_record:?}"
);
assert_eq!(bd_record["kind"].as_str(), Some("llm_call"));
info!("conformance_audit_record_with_issue passed");
}
#[test]
fn conformance_audit_label() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_audit_label test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_record = workspace.run_br(
[
"audit",
"record",
"--kind",
"llm_call",
"--model",
"test",
"--prompt",
"test",
"--response",
"test",
"--json",
],
"audit_record_for_label",
);
let bd_record = workspace.run_bd(
[
"audit",
"record",
"--kind",
"llm_call",
"--model",
"test",
"--prompt",
"test",
"--response",
"test",
"--json",
],
"audit_record_for_label",
);
assert_command_success(&br_record, "br audit record for label");
assert_command_success(&bd_record, "bd audit record for label");
let br_entry_id = extract_audit_entry_id(&br_record.stdout);
let bd_entry_id = extract_audit_entry_id(&bd_record.stdout);
info!("br entry_id: {}, bd entry_id: {}", br_entry_id, bd_entry_id);
let br_out = workspace.run_br(
[
"audit",
"label",
&br_entry_id,
"--label",
"good",
"--reason",
"Test label",
"--json",
],
"audit_label",
);
let bd_out = workspace.run_bd(
[
"audit",
"label",
&bd_entry_id,
"--label",
"good",
"--reason",
"Test label",
"--json",
],
"audit_label",
);
info!(
"br audit label: success={}, bd audit label: success={}",
br_out.status.success(),
bd_out.status.success()
);
assert_command_success(&br_out, "br audit label");
assert_command_success(&bd_out, "bd audit label");
let br_label: Value =
serde_json::from_str(&extract_json_payload(&br_out.stdout)).expect("br audit label json");
let bd_label: Value =
serde_json::from_str(&extract_json_payload(&bd_out.stdout)).expect("bd audit label json");
assert_eq!(br_label["parent_id"].as_str(), Some(br_entry_id.as_str()));
assert_eq!(br_label["label"].as_str(), Some("good"));
assert!(
br_label["id"].as_str().is_some_and(|id| !id.is_empty()),
"br audit label should return a label entry id: {br_label:?}"
);
assert_eq!(bd_label["parent_id"].as_str(), Some(bd_entry_id.as_str()));
assert_eq!(bd_label["label"].as_str(), Some("good"));
assert!(
bd_label["id"].as_str().is_some_and(|id| !id.is_empty()),
"bd audit label should return a label entry id: {bd_label:?}"
);
info!("conformance_audit_label passed");
}
#[test]
fn conformance_audit_record_with_error() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_audit_record_with_error test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_out = workspace.run_br(
[
"audit",
"record",
"--kind",
"tool_call",
"--tool-name",
"bash",
"--exit-code",
"1",
"--error",
"Command failed: permission denied",
"--json",
],
"audit_error",
);
let bd_out = workspace.run_bd(
[
"audit",
"record",
"--kind",
"tool_call",
"--tool-name",
"bash",
"--exit-code",
"1",
"--error",
"Command failed: permission denied",
"--json",
],
"audit_error",
);
info!(
"br audit record with error: success={}, bd audit record with error: success={}",
br_out.status.success(),
bd_out.status.success()
);
assert!(
br_out.status.success(),
"br audit record with error failed: {}",
br_out.stderr
);
assert!(
bd_out.status.success(),
"bd audit record with error failed: {}",
bd_out.stderr
);
info!("conformance_audit_record_with_error passed");
}
fn extract_audit_entry_id(output: &str) -> String {
let json = extract_json_payload(output);
let v: Value = serde_json::from_str(&json).expect("parse audit entry json");
v.get("id")
.or_else(|| v.get("entry_id"))
.and_then(|value| value.as_str())
.expect("audit entry JSON should include id or entry_id")
.to_string()
}
#[test]
fn conformance_q_basic() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_q_basic test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_q = workspace.run_br(["q", "Quick capture test"], "q_basic");
let bd_q = workspace.run_bd(["q", "Quick capture test"], "q_basic");
assert!(br_q.status.success(), "br q failed: {}", br_q.stderr);
assert!(bd_q.status.success(), "bd q failed: {}", bd_q.stderr);
let br_id = br_q.stdout.trim();
let bd_id = bd_q.stdout.trim();
assert!(!br_id.is_empty(), "br q should output an ID");
assert!(!bd_id.is_empty(), "bd q should output an ID");
info!("br q ID: {}, bd q ID: {}", br_id, bd_id);
info!("conformance_q_basic passed");
}
#[test]
fn conformance_q_with_priority() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_q_with_priority test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_q = workspace.run_br(["q", "High priority quick", "-p", "1"], "q_priority");
let bd_q = workspace.run_bd(["q", "High priority quick", "-p", "1"], "q_priority");
assert!(
br_q.status.success(),
"br q with priority failed: {}",
br_q.stderr
);
assert!(
bd_q.status.success(),
"bd q with priority failed: {}",
bd_q.stderr
);
info!("conformance_q_with_priority passed");
}
#[test]
fn conformance_q_with_type() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_q_with_type test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_q = workspace.run_br(["q", "Bug quick capture", "-t", "bug"], "q_type");
let bd_q = workspace.run_bd(["q", "Bug quick capture", "-t", "bug"], "q_type");
assert!(
br_q.status.success(),
"br q with type failed: {}",
br_q.stderr
);
assert!(
bd_q.status.success(),
"bd q with type failed: {}",
bd_q.stderr
);
info!("conformance_q_with_type passed");
}
#[test]
fn conformance_q_creates_issue() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_q_creates_issue test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_q = workspace.run_br(["q", "Verify creation"], "q_create");
let br_id = br_q.stdout.trim();
let bd_q = workspace.run_bd(["q", "Verify creation"], "q_create");
let bd_id = bd_q.stdout.trim();
let br_show = workspace.run_br(["show", br_id, "--json"], "show_q_issue");
let bd_show = workspace.run_bd(["show", bd_id, "--json"], "show_q_issue");
assert!(
br_show.status.success(),
"br show q-created issue failed: {}",
br_show.stderr
);
assert!(
bd_show.status.success(),
"bd show q-created issue failed: {}",
bd_show.stderr
);
info!("conformance_q_creates_issue passed");
}
#[test]
fn conformance_q_id_in_list() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_q_id_in_list test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_q = workspace.run_br(["q", "List me"], "q_list");
let bd_q = workspace.run_bd(["q", "List me"], "q_list");
let br_id = br_q.stdout.trim().to_string();
let bd_id = bd_q.stdout.trim().to_string();
let br_list = workspace.run_br(["list", "--json"], "q_list_br");
let bd_list = workspace.run_bd(["list", "--json"], "q_list_bd");
assert!(
br_list.status.success(),
"br list failed: {}",
br_list.stderr
);
assert!(
bd_list.status.success(),
"bd list failed: {}",
bd_list.stderr
);
let br_val: Value = serde_json::from_str(&extract_json_payload(&br_list.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&extract_json_payload(&bd_list.stdout))
.unwrap_or(Value::Array(vec![]));
let br_ids: Vec<&str> = br_val
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.get("id").and_then(|id| id.as_str()))
.collect()
})
.unwrap_or_default();
let bd_ids: Vec<&str> = bd_val
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.get("id").and_then(|id| id.as_str()))
.collect()
})
.unwrap_or_default();
assert!(br_ids.contains(&br_id.as_str()));
assert!(bd_ids.contains(&bd_id.as_str()));
info!("conformance_q_id_in_list passed");
}
#[test]
fn conformance_q_error_no_title() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_q_error_no_title test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_q = workspace.run_br(["q"], "q_no_title");
let bd_q = workspace.run_bd(["q"], "q_no_title");
assert!(!br_q.status.success(), "br q should fail without title");
assert!(!bd_q.status.success(), "bd q should fail without title");
info!("conformance_q_error_no_title passed");
}
#[test]
fn conformance_lint_empty() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_lint_empty test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_lint = workspace.run_br(["lint", "--json"], "lint_empty");
let bd_lint = workspace.run_bd(["lint", "--json"], "lint_empty");
assert!(
br_lint.status.success(),
"br lint empty failed: {}",
br_lint.stderr
);
assert!(
bd_lint.status.success(),
"bd lint empty failed: {}",
bd_lint.stderr
);
info!("conformance_lint_empty passed");
}
#[test]
fn conformance_lint_with_issues() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_lint_with_issues test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Test issue for lint"], "create");
workspace.run_bd(["create", "Test issue for lint"], "create");
let br_lint = workspace.run_br(["lint", "--json"], "lint_with_issues");
let bd_lint = workspace.run_bd(["lint", "--json"], "lint_with_issues");
assert!(
br_lint.status.success(),
"br lint failed: {}",
br_lint.stderr
);
assert!(
bd_lint.status.success(),
"bd lint failed: {}",
bd_lint.stderr
);
info!("conformance_lint_with_issues passed");
}
#[test]
fn conformance_lint_by_type() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_lint_by_type test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Bug issue", "-t", "bug"], "create_bug");
workspace.run_bd(["create", "Bug issue", "-t", "bug"], "create_bug");
workspace.run_br(["create", "Task issue", "-t", "task"], "create_task");
workspace.run_bd(["create", "Task issue", "-t", "task"], "create_task");
let br_lint = workspace.run_br(["lint", "-t", "bug", "--json"], "lint_by_type");
let bd_lint = workspace.run_bd(["lint", "-t", "bug", "--json"], "lint_by_type");
assert!(
br_lint.status.success(),
"br lint by type failed: {}",
br_lint.stderr
);
assert!(
bd_lint.status.success(),
"bd lint by type failed: {}",
bd_lint.stderr
);
info!("conformance_lint_by_type passed");
}
#[test]
fn conformance_lint_json_shape() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_lint_json_shape test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Lint test"], "create");
workspace.run_bd(["create", "Lint test"], "create");
let br_lint = workspace.run_br(["lint", "--json"], "lint_json");
let bd_lint = workspace.run_bd(["lint", "--json"], "lint_json");
let br_json = extract_json_payload(&br_lint.stdout);
let bd_json = extract_json_payload(&bd_lint.stdout);
let br_val: Result<Value, _> = serde_json::from_str(&br_json);
let bd_val: Result<Value, _> = serde_json::from_str(&bd_json);
assert!(br_val.is_ok(), "br lint should produce valid JSON");
assert!(bd_val.is_ok(), "bd lint should produce valid JSON");
info!("conformance_lint_json_shape passed");
}
#[test]
fn conformance_lint_exit_code() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_lint_exit_code test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Lint bug", "--type", "bug"], "lint_bug_create");
workspace.run_bd(["create", "Lint bug", "--type", "bug"], "lint_bug_create");
let br_lint = workspace.run_br(["lint"], "lint_exit");
let bd_lint = workspace.run_bd(["lint"], "lint_exit");
assert!(
!br_lint.status.success(),
"br lint should exit nonzero with warnings"
);
assert!(
!bd_lint.status.success(),
"bd lint should exit nonzero with warnings"
);
info!("conformance_lint_exit_code passed");
}
#[test]
fn conformance_defer_basic() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_defer_basic test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Defer test", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Defer test", "--json"], "create");
let br_id = extract_issue_id(&extract_json_payload(&br_create.stdout));
let bd_id = extract_issue_id(&extract_json_payload(&bd_create.stdout));
let br_defer = workspace.run_br(["defer", &br_id, "--until", "+1d", "--json"], "defer_basic");
let bd_defer = workspace.run_bd(["defer", &bd_id, "--until", "+1d", "--json"], "defer_basic");
assert!(
br_defer.status.success(),
"br defer failed: {}",
br_defer.stderr
);
assert!(
bd_defer.status.success(),
"bd defer failed: {}",
bd_defer.stderr
);
info!("conformance_defer_basic passed");
}
#[test]
fn conformance_defer_excludes_from_ready() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_defer_excludes_from_ready test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Will defer", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Will defer", "--json"], "create");
let br_id = extract_issue_id(&extract_json_payload(&br_create.stdout));
let bd_id = extract_issue_id(&extract_json_payload(&bd_create.stdout));
workspace.run_br(["defer", &br_id, "--until", "+30d"], "defer");
workspace.run_bd(["defer", &bd_id, "--until", "+30d"], "defer");
let br_ready = workspace.run_br(["ready", "--json"], "ready_after_defer");
let bd_ready = workspace.run_bd(["ready", "--json"], "ready_after_defer");
assert!(br_ready.status.success(), "br ready failed");
assert!(bd_ready.status.success(), "bd ready failed");
let br_json = extract_json_payload(&br_ready.stdout);
let bd_json = extract_json_payload(&bd_ready.stdout);
assert!(
!br_json.contains(&br_id),
"br ready should not include deferred issue"
);
assert!(
!bd_json.contains(&bd_id),
"bd ready should not include deferred issue"
);
info!("conformance_defer_excludes_from_ready passed");
}
#[test]
fn conformance_undefer_basic() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_undefer_basic test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Undefer test", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Undefer test", "--json"], "create");
let br_id = extract_issue_id(&extract_json_payload(&br_create.stdout));
let bd_id = extract_issue_id(&extract_json_payload(&bd_create.stdout));
workspace.run_br(["defer", &br_id, "--until", "+30d"], "defer");
workspace.run_bd(["defer", &bd_id, "--until", "+30d"], "defer");
let br_undefer = workspace.run_br(["undefer", &br_id, "--json"], "undefer");
let bd_undefer = workspace.run_bd(["undefer", &bd_id, "--json"], "undefer");
assert!(
br_undefer.status.success(),
"br undefer failed: {}",
br_undefer.stderr
);
assert!(
bd_undefer.status.success(),
"bd undefer failed: {}",
bd_undefer.stderr
);
info!("conformance_undefer_basic passed");
}
#[test]
fn conformance_undefer_restores_ready() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_undefer_restores_ready test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Restore to ready", "--json"], "create");
let bd_create = workspace.run_bd(["create", "Restore to ready", "--json"], "create");
let br_id = extract_issue_id(&extract_json_payload(&br_create.stdout));
let bd_id = extract_issue_id(&extract_json_payload(&bd_create.stdout));
workspace.run_br(["defer", &br_id, "--until", "+30d"], "defer");
workspace.run_bd(["defer", &bd_id, "--until", "+30d"], "defer");
workspace.run_br(["undefer", &br_id], "undefer");
workspace.run_bd(["undefer", &bd_id], "undefer");
let br_ready = workspace.run_br(["ready", "--json"], "ready_after_undefer");
let bd_ready = workspace.run_bd(["ready", "--json"], "ready_after_undefer");
assert!(br_ready.status.success(), "br ready failed");
assert!(bd_ready.status.success(), "bd ready failed");
info!("conformance_undefer_restores_ready passed");
}
#[test]
fn conformance_history_list_empty() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_history_list_empty test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_hist = workspace.run_br(["history", "list", "--json"], "history_list_empty");
assert!(
br_hist.status.success(),
"br history list failed: {}",
br_hist.stderr
);
info!("conformance_history_list_empty passed");
}
#[test]
fn conformance_history_list_after_sync() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_history_list_after_sync test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "History test"], "create");
workspace.run_br(["sync", "--flush-only"], "sync");
let br_hist = workspace.run_br(["history", "list", "--json"], "history_list");
assert!(
br_hist.status.success(),
"br history list failed: {}",
br_hist.stderr
);
info!("conformance_history_list_after_sync passed");
}
#[test]
fn conformance_history_json_shape() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_history_json_shape test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_hist = workspace.run_br(["history", "list", "--json"], "history_json");
assert!(
br_hist.status.success(),
"br history list failed: {}",
br_hist.stderr
);
let br_json = extract_json_payload(&br_hist.stdout);
if !br_json.is_empty() && !br_json.contains("No backups found") {
let br_val: Result<Value, _> = serde_json::from_str(&br_json);
assert!(
br_val.is_ok(),
"br history list should produce valid JSON when backups exist"
);
}
info!("conformance_history_json_shape passed");
}
#[test]
fn conformance_orphans_empty() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_orphans_empty test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_orphans = workspace.run_br(["orphans", "--json"], "orphans_empty");
let bd_orphans = workspace.run_bd(["orphans", "--json"], "orphans_empty");
assert!(
br_orphans.status.success(),
"br orphans failed: {}",
br_orphans.stderr
);
assert!(
bd_orphans.status.success(),
"bd orphans failed: {}",
bd_orphans.stderr
);
info!("conformance_orphans_empty passed");
}
#[test]
fn conformance_orphans_with_issues() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_orphans_with_issues test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Orphan test"], "create");
workspace.run_bd(["create", "Orphan test"], "create");
let br_orphans = workspace.run_br(["orphans", "--json"], "orphans_with_issues");
let bd_orphans = workspace.run_bd(["orphans", "--json"], "orphans_with_issues");
assert!(
br_orphans.status.success(),
"br orphans failed: {}",
br_orphans.stderr
);
assert!(
bd_orphans.status.success(),
"bd orphans failed: {}",
bd_orphans.stderr
);
info!("conformance_orphans_with_issues passed");
}
#[test]
fn conformance_orphans_json_shape() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_orphans_json_shape test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_orphans = workspace.run_br(["orphans", "--json"], "orphans_json");
let bd_orphans = workspace.run_bd(["orphans", "--json"], "orphans_json");
let br_json = extract_json_payload(&br_orphans.stdout);
let bd_json = extract_json_payload(&bd_orphans.stdout);
let br_val: Result<Value, _> = serde_json::from_str(&br_json);
let bd_val: Result<Value, _> = serde_json::from_str(&bd_json);
assert!(br_val.is_ok(), "br orphans should produce valid JSON");
assert!(bd_val.is_ok(), "bd orphans should produce valid JSON");
info!("conformance_orphans_json_shape passed");
}
#[test]
fn conformance_changelog_empty() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_changelog_empty test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_changelog = workspace.run_br(["changelog", "--json"], "changelog_empty");
assert!(
br_changelog.status.success(),
"br changelog failed: {}",
br_changelog.stderr
);
info!("conformance_changelog_empty passed");
}
#[test]
fn conformance_changelog_with_closed() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_changelog_with_closed test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_create = workspace.run_br(["create", "Changelog entry", "--json"], "create");
let br_id = extract_issue_id(&extract_json_payload(&br_create.stdout));
workspace.run_br(["close", &br_id], "close");
let br_changelog = workspace.run_br(["changelog", "--json"], "changelog_with_closed");
assert!(
br_changelog.status.success(),
"br changelog failed: {}",
br_changelog.stderr
);
info!("conformance_changelog_with_closed passed");
}
#[test]
fn conformance_changelog_json_shape() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_changelog_json_shape test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_changelog = workspace.run_br(["changelog", "--json"], "changelog_json");
let br_json = extract_json_payload(&br_changelog.stdout);
let br_val: Result<Value, _> = serde_json::from_str(&br_json);
assert!(br_val.is_ok(), "br changelog should produce valid JSON");
info!("conformance_changelog_json_shape passed");
}
#[test]
fn conformance_query_list_empty() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_query_list_empty test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_query = workspace.run_br(["query", "list", "--json"], "query_list_empty");
assert!(
br_query.status.success(),
"br query list failed: {}",
br_query.stderr
);
info!("conformance_query_list_empty passed");
}
#[test]
fn conformance_query_save_and_list() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_query_save_and_list test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_save = workspace.run_br(
[
"query",
"save",
"high-priority",
"--status",
"open",
"--priority",
"1",
"--json",
],
"query_save",
);
assert!(
br_save.status.success(),
"br query save failed: {}",
br_save.stderr
);
let br_list = workspace.run_br(["query", "list", "--json"], "query_list");
assert!(br_list.status.success(), "br query list failed");
info!("conformance_query_save_and_list passed");
}
#[test]
fn conformance_query_run() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_query_run test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "High pri", "-p", "1"], "create_high");
workspace.run_br(["create", "Low pri", "-p", "3"], "create_low");
workspace.run_br(
["query", "save", "high-only", "--priority", "1"],
"query_save",
);
let br_run = workspace.run_br(["query", "run", "high-only", "--json"], "query_run");
assert!(
br_run.status.success(),
"br query run failed: {}",
br_run.stderr
);
info!("conformance_query_run passed");
}
#[test]
fn conformance_query_delete() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_query_delete test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(
["query", "save", "to-delete", "--status", "open"],
"query_save",
);
let br_delete = workspace.run_br(["query", "delete", "to-delete", "--json"], "query_delete");
assert!(
br_delete.status.success(),
"br query delete failed: {}",
br_delete.stderr
);
info!("conformance_query_delete passed");
}
#[test]
fn conformance_completions_bash() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_completions_bash test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_comp = workspace.run_br(["completions", "bash"], "completions_bash");
let bd_comp = workspace.run_bd(["completion", "bash"], "completion_bash");
assert!(
br_comp.status.success(),
"br completions bash failed: {}",
br_comp.stderr
);
assert!(
bd_comp.status.success(),
"bd completion bash failed: {}",
bd_comp.stderr
);
assert!(
!br_comp.stdout.is_empty(),
"br completions should produce output"
);
assert!(
!bd_comp.stdout.is_empty(),
"bd completion should produce output"
);
info!("conformance_completions_bash passed");
}
#[test]
fn conformance_completions_zsh() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_completions_zsh test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_comp = workspace.run_br(["completions", "zsh"], "completions_zsh");
let bd_comp = workspace.run_bd(["completion", "zsh"], "completion_zsh");
assert!(
br_comp.status.success(),
"br completions zsh failed: {}",
br_comp.stderr
);
assert!(
bd_comp.status.success(),
"bd completion zsh failed: {}",
bd_comp.stderr
);
info!("conformance_completions_zsh passed");
}
#[test]
fn conformance_completions_fish() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_completions_fish test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_comp = workspace.run_br(["completions", "fish"], "completions_fish");
let bd_comp = workspace.run_bd(["completion", "fish"], "completion_fish");
assert!(
br_comp.status.success(),
"br completions fish failed: {}",
br_comp.stderr
);
assert!(
bd_comp.status.success(),
"bd completion fish failed: {}",
bd_comp.stderr
);
info!("conformance_completions_fish passed");
}
#[test]
fn conformance_stats_all_fields() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_stats_all_fields test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Issue"], "create");
workspace.run_bd(["create", "Issue"], "create");
let br_stats = workspace.run_br(["stats", "--json"], "stats");
let bd_stats = workspace.run_bd(["stats", "--json"], "stats");
assert!(br_stats.status.success());
assert!(bd_stats.status.success());
let br_json = extract_json_payload(&br_stats.stdout);
let bd_json = extract_json_payload(&bd_stats.stdout);
compare_json(
&br_json,
&bd_json,
&CompareMode::ContainsFields(vec![
"summary.total_issues".to_string(),
"summary.open_issues".to_string(),
"summary.in_progress_issues".to_string(),
"summary.closed_issues".to_string(),
"summary.blocked_issues".to_string(),
"summary.deferred_issues".to_string(),
"summary.ready_issues".to_string(),
"summary.tombstone_issues".to_string(),
"summary.pinned_issues".to_string(),
"summary.epics_eligible_for_closure".to_string(),
]),
)
.expect("JSON mismatch");
info!("conformance_stats_all_fields passed");
}
#[test]
#[ignore]
fn conformance_stale_all_stale() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_stale_all_stale test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["config", "--set", "id.prefix=TEST"], "set_prefix_br");
workspace.run_bd(["config", "--set", "id.prefix=TEST"], "set_prefix_bd");
workspace.run_br(["create", "Stale issue"], "create");
workspace.run_bd(["create", "Stale issue"], "create");
std::thread::sleep(Duration::from_millis(100));
let br_stale = workspace.run_br(["stale", "--days", "0", "--json"], "stale");
let bd_stale = workspace.run_bd(["stale", "--days", "0", "--json"], "stale");
assert!(br_stale.status.success());
assert!(bd_stale.status.success());
let br_json = extract_json_payload(&br_stale.stdout);
let bd_json = extract_json_payload(&bd_stale.stdout);
log_timings("stale_all_stale", &br_stale, &bd_stale);
compare_json(&br_json, &bd_json, &CompareMode::NormalizedJson).expect("JSON mismatch");
info!("conformance_stale_all_stale passed");
}
#[test]
fn conformance_version_semver() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_version_semver test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_ver = workspace.run_br(["version", "--json"], "version");
let bd_ver = workspace.run_bd(["version", "--json"], "version");
let br_json = extract_json_payload(&br_ver.stdout);
let bd_json = extract_json_payload(&bd_ver.stdout);
let br_val: Value = serde_json::from_str(&br_json).unwrap();
let bd_val: Value = serde_json::from_str(&bd_json).unwrap();
let br_version = br_val["version"].as_str().unwrap_or("");
let bd_version = bd_val["version"].as_str().unwrap_or("");
let is_semver = |v: &str| {
let parts: Vec<&str> = v.split('.').collect();
parts.len() >= 2
&& parts
.iter()
.all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit()))
};
assert!(
is_semver(br_version),
"br version is not semver: {br_version}"
);
assert!(
is_semver(bd_version),
"bd version is not semver: {bd_version}"
);
log_timings("version_semver", &br_ver, &bd_ver);
info!("conformance_version_semver passed");
}
fn init_git_repo(dir: &PathBuf) {
std::process::Command::new("git")
.args(["init"])
.current_dir(dir)
.output()
.expect("git init");
std::process::Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(dir)
.output()
.expect("git config email");
std::process::Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(dir)
.output()
.expect("git config name");
}
#[test]
#[ignore = "bd v0.46.0 sync does git commit, fails in non-git dirs: br is intentionally non-invasive"]
fn conformance_sync_base_snapshot_created_after_sync() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_base_snapshot_created_after_sync test");
let workspace = ConformanceWorkspace::new();
init_git_repo(&workspace.br_root);
init_git_repo(&workspace.bd_root);
workspace.init_both();
workspace.run_br(["create", "Base snapshot test"], "create");
workspace.run_bd(["create", "Base snapshot test"], "create");
let br_flush = workspace.run_br(["sync", "--flush-only"], "flush");
let bd_flush = workspace.run_bd(["sync", "--flush-only"], "flush");
assert!(
br_flush.status.success(),
"br flush failed: {}",
br_flush.stderr
);
assert!(
bd_flush.status.success(),
"bd flush failed: {}",
bd_flush.stderr
);
std::process::Command::new("git")
.args(["add", ".beads/"])
.current_dir(&workspace.br_root)
.output()
.expect("git add br");
std::process::Command::new("git")
.args(["commit", "-m", "initial"])
.current_dir(&workspace.br_root)
.output()
.expect("git commit br");
std::process::Command::new("git")
.args(["add", ".beads/"])
.current_dir(&workspace.bd_root)
.output()
.expect("git add bd");
std::process::Command::new("git")
.args(["commit", "-m", "initial"])
.current_dir(&workspace.bd_root)
.output()
.expect("git commit bd");
let br_sync = workspace.run_br(["sync"], "sync");
let bd_sync = workspace.run_bd(["sync"], "sync");
assert!(
br_sync.status.success(),
"br sync failed: {}",
br_sync.stderr
);
assert!(
bd_sync.status.success(),
"bd sync failed: {}",
bd_sync.stderr
);
let br_base = workspace.br_root.join(".beads").join("beads.base.jsonl");
let bd_base = workspace.bd_root.join(".beads").join("beads.base.jsonl");
let br_base_exists = br_base.exists();
let bd_base_exists = bd_base.exists();
assert_eq!(
br_base_exists, bd_base_exists,
"base snapshot existence differs: br={}, bd={}",
br_base_exists, bd_base_exists
);
info!("conformance_sync_base_snapshot_created_after_sync passed");
}
#[test]
fn conformance_sync_base_snapshot_content_matches() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_base_snapshot_content_matches test");
let workspace = ConformanceWorkspace::new();
init_git_repo(&workspace.br_root);
init_git_repo(&workspace.bd_root);
workspace.init_both();
workspace.run_br(["config", "--set", "id.prefix=TEST"], "set_prefix_br");
workspace.run_bd(["config", "--set", "id.prefix=TEST"], "set_prefix_bd");
workspace.run_br(["create", "Base content test"], "create");
workspace.run_bd(["create", "Base content test"], "create");
workspace.run_br(["sync", "--flush-only"], "flush");
workspace.run_bd(["sync", "--flush-only"], "flush");
std::process::Command::new("git")
.args(["add", ".beads/"])
.current_dir(&workspace.br_root)
.output()
.expect("git add br");
std::process::Command::new("git")
.args(["commit", "-m", "initial"])
.current_dir(&workspace.br_root)
.output()
.expect("git commit br");
std::process::Command::new("git")
.args(["add", ".beads/"])
.current_dir(&workspace.bd_root)
.output()
.expect("git add bd");
std::process::Command::new("git")
.args(["commit", "-m", "initial"])
.current_dir(&workspace.bd_root)
.output()
.expect("git commit bd");
workspace.run_br(["sync"], "sync");
workspace.run_bd(["sync"], "sync");
let br_base = workspace.br_root.join(".beads").join("beads.base.jsonl");
let bd_base = workspace.bd_root.join(".beads").join("beads.base.jsonl");
let br_content = fs::read_to_string(&br_base).ok();
let bd_content = fs::read_to_string(&bd_base).ok();
match (br_content, bd_content) {
(Some(br), Some(bd)) => {
let br_lines: Vec<&str> = br.lines().filter(|l| !l.trim().is_empty()).collect();
let bd_lines: Vec<&str> = bd.lines().filter(|l| !l.trim().is_empty()).collect();
assert_eq!(
br_lines.len(),
bd_lines.len(),
"base snapshot line count differs: br={}, bd={}",
br_lines.len(),
bd_lines.len()
);
}
(None, None) => {
info!("Both br and bd did not create base snapshot (consistent behavior)");
}
(br, bd) => {
panic!(
"base snapshot creation differs: br={:?}, bd={:?}",
br.is_some(),
bd.is_some()
);
}
}
info!("conformance_sync_base_snapshot_content_matches passed");
}
#[test]
fn conformance_sync_base_snapshot_preserves_issue_state() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_base_snapshot_preserves_issue_state test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Issue 1"], "create1");
workspace.run_bd(["create", "Issue 1"], "create1");
workspace.run_br(["create", "Issue 2"], "create2");
workspace.run_bd(["create", "Issue 2"], "create2");
workspace.run_br(["sync", "--flush-only"], "flush");
workspace.run_bd(["sync", "--flush-only"], "flush");
let br_list = workspace.run_br(["list", "--json"], "list_open");
let bd_list = workspace.run_bd(["list", "--json"], "list_open");
let br_json = extract_json_payload(&br_list.stdout);
let bd_json = extract_json_payload(&bd_list.stdout);
let br_val: Value = serde_json::from_str(&br_json).unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&bd_json).unwrap_or(Value::Array(vec![]));
let br_count = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_count = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_count, bd_count,
"issue count after flush differs: br={}, bd={}",
br_count, bd_count
);
assert_eq!(br_count, 2, "expected 2 open issues after flush");
info!("conformance_sync_base_snapshot_preserves_issue_state passed");
}
#[test]
fn conformance_sync_import_rejects_conflict_markers() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_import_rejects_conflict_markers test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Valid issue"], "create");
workspace.run_bd(["create", "Valid issue"], "create");
workspace.run_br(["sync", "--flush-only"], "flush");
workspace.run_bd(["sync", "--flush-only"], "flush");
let br_jsonl_path = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_jsonl_path = workspace.bd_root.join(".beads").join("issues.jsonl");
let br_content = fs::read_to_string(&br_jsonl_path).expect("read br jsonl");
let bd_content = fs::read_to_string(&bd_jsonl_path).expect("read bd jsonl");
let br_conflicted = format!(
"<<<<<<< HEAD\n{}\n=======\n{}\n>>>>>>> feature-branch\n",
br_content.trim(),
br_content.trim()
);
let bd_conflicted = format!(
"<<<<<<< HEAD\n{}\n=======\n{}\n>>>>>>> feature-branch\n",
bd_content.trim(),
bd_content.trim()
);
fs::write(&br_jsonl_path, &br_conflicted).expect("write br conflicted");
fs::write(&bd_jsonl_path, &bd_conflicted).expect("write bd conflicted");
let br_import = workspace.run_br(["sync", "--import-only"], "import_conflict");
let bd_import = workspace.run_bd(["sync", "--import-only"], "import_conflict");
assert!(
!br_import.status.success(),
"br should reject conflict markers but succeeded"
);
assert!(
!bd_import.status.success(),
"bd should reject conflict markers but succeeded"
);
let br_mentions_conflict = br_import.stderr.to_lowercase().contains("conflict")
|| br_import.stdout.to_lowercase().contains("conflict");
let bd_mentions_conflict = bd_import.stderr.to_lowercase().contains("conflict")
|| bd_import.stdout.to_lowercase().contains("conflict");
assert!(
br_mentions_conflict,
"br error should mention conflict: stdout={}, stderr={}",
br_import.stdout, br_import.stderr
);
assert!(
bd_mentions_conflict,
"bd error should mention conflict: stdout={}, stderr={}",
bd_import.stdout, bd_import.stderr
);
info!("conformance_sync_import_rejects_conflict_markers passed");
}
#[test]
fn conformance_sync_import_rejects_partial_conflict_markers() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_import_rejects_partial_conflict_markers test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
let br_jsonl_path = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_jsonl_path = workspace.bd_root.join(".beads").join("issues.jsonl");
let partial_conflict = "<<<<<<< HEAD\n{\"id\":\"test-1\",\"title\":\"Test\"}\n";
fs::write(&br_jsonl_path, partial_conflict).expect("write br partial conflict");
fs::write(&bd_jsonl_path, partial_conflict).expect("write bd partial conflict");
let br_import = workspace.run_br(["sync", "--import-only"], "import_partial_conflict");
let bd_import = workspace.run_bd(["sync", "--import-only"], "import_partial_conflict");
assert_eq!(
br_import.status.success(),
bd_import.status.success(),
"partial conflict marker handling differs: br={}, bd={}",
br_import.status.success(),
bd_import.status.success()
);
if !br_import.status.success() && !bd_import.status.success() {
let br_mentions = br_import.stderr.to_lowercase().contains("conflict")
|| br_import.stderr.contains("<<<<<<<");
let bd_mentions = bd_import.stderr.to_lowercase().contains("conflict")
|| bd_import.stderr.contains("<<<<<<<");
assert!(
br_mentions || bd_mentions,
"at least one should mention conflict markers"
);
}
info!("conformance_sync_import_rejects_partial_conflict_markers passed");
}
#[test]
fn conformance_sync_import_rejects_conflict_in_middle() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_import_rejects_conflict_in_middle test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["create", "Issue 1"], "create1");
workspace.run_bd(["create", "Issue 1"], "create1");
workspace.run_br(["create", "Issue 2"], "create2");
workspace.run_bd(["create", "Issue 2"], "create2");
workspace.run_br(["sync", "--flush-only"], "flush");
workspace.run_bd(["sync", "--flush-only"], "flush");
let br_jsonl_path = workspace.br_root.join(".beads").join("issues.jsonl");
let bd_jsonl_path = workspace.bd_root.join(".beads").join("issues.jsonl");
let br_content = fs::read_to_string(&br_jsonl_path).expect("read br jsonl");
let bd_content = fs::read_to_string(&bd_jsonl_path).expect("read bd jsonl");
let br_lines: Vec<&str> = br_content.lines().collect();
let bd_lines: Vec<&str> = bd_content.lines().collect();
let br_with_conflict = if br_lines.len() >= 2 {
format!(
"{}\n<<<<<<< HEAD\n{}\n=======\n>>>>>>> branch\n",
br_lines[0], br_lines[1]
)
} else {
format!("<<<<<<< HEAD\n{}\n=======\n>>>>>>> branch\n", br_content)
};
let bd_with_conflict = if bd_lines.len() >= 2 {
format!(
"{}\n<<<<<<< HEAD\n{}\n=======\n>>>>>>> branch\n",
bd_lines[0], bd_lines[1]
)
} else {
format!("<<<<<<< HEAD\n{}\n=======\n>>>>>>> branch\n", bd_content)
};
fs::write(&br_jsonl_path, &br_with_conflict).expect("write br conflict");
fs::write(&bd_jsonl_path, &bd_with_conflict).expect("write bd conflict");
let br_import = workspace.run_br(["sync", "--import-only"], "import_middle_conflict");
let bd_import = workspace.run_bd(["sync", "--import-only"], "import_middle_conflict");
assert_eq!(
br_import.status.success(),
bd_import.status.success(),
"middle conflict marker handling differs: br success={}, bd success={}",
br_import.status.success(),
bd_import.status.success()
);
info!("conformance_sync_import_rejects_conflict_in_middle passed");
}
#[test]
fn conformance_sync_import_prefix_mismatch_behavior() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_import_prefix_mismatch_behavior test");
let source = ConformanceWorkspace::new();
source.init_both();
source.run_br(["config", "--set", "id.prefix=SRC"], "set_prefix_br");
source.run_bd(["config", "--set", "id.prefix=SRC"], "set_prefix_bd");
source.run_br(["create", "Source issue"], "create");
source.run_bd(["create", "Source issue"], "create");
source.run_br(["sync", "--flush-only"], "flush");
source.run_bd(["sync", "--flush-only"], "flush");
let target = ConformanceWorkspace::new();
target.init_both();
target.run_br(["config", "--set", "id.prefix=TGT"], "set_prefix_br");
target.run_bd(["config", "--set", "id.prefix=TGT"], "set_prefix_bd");
let br_src = source.br_root.join(".beads").join("issues.jsonl");
let bd_src = source.bd_root.join(".beads").join("issues.jsonl");
let br_dst = target.br_root.join(".beads").join("issues.jsonl");
let bd_dst = target.bd_root.join(".beads").join("issues.jsonl");
fs::copy(&br_src, &br_dst).expect("copy br jsonl");
fs::copy(&bd_src, &bd_dst).expect("copy bd jsonl");
let br_import = target.run_br(["sync", "--import-only"], "import_mismatch");
let bd_import = target.run_bd(["sync", "--import-only"], "import_mismatch");
assert_eq!(
br_import.status.success(),
bd_import.status.success(),
"prefix mismatch handling differs: br success={}, bd success={}",
br_import.status.success(),
bd_import.status.success()
);
if !br_import.status.success() && !bd_import.status.success() {
let br_mentions_prefix = br_import.stderr.to_lowercase().contains("prefix")
|| br_import.stdout.to_lowercase().contains("prefix");
let bd_mentions_prefix = bd_import.stderr.to_lowercase().contains("prefix")
|| bd_import.stdout.to_lowercase().contains("prefix");
assert!(
br_mentions_prefix || bd_mentions_prefix,
"error should mention prefix mismatch"
);
}
info!("conformance_sync_import_prefix_mismatch_behavior passed");
}
#[test]
fn conformance_sync_import_same_prefix_succeeds() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_import_same_prefix_succeeds test");
let source = ConformanceWorkspace::new();
source.init_both();
source.run_br(["config", "--set", "id.prefix=SAME"], "set_prefix_br");
source.run_bd(["config", "--set", "id.prefix=SAME"], "set_prefix_bd");
source.run_br(["create", "Same prefix issue"], "create");
source.run_bd(["create", "Same prefix issue"], "create");
source.run_br(["sync", "--flush-only"], "flush");
source.run_bd(["sync", "--flush-only"], "flush");
let target = ConformanceWorkspace::new();
target.init_both();
target.run_br(["config", "--set", "id.prefix=SAME"], "set_prefix_br");
target.run_bd(["config", "--set", "id.prefix=SAME"], "set_prefix_bd");
let br_src = source.br_root.join(".beads").join("issues.jsonl");
let bd_src = source.bd_root.join(".beads").join("issues.jsonl");
let br_dst = target.br_root.join(".beads").join("issues.jsonl");
let bd_dst = target.bd_root.join(".beads").join("issues.jsonl");
fs::copy(&br_src, &br_dst).expect("copy br jsonl");
fs::copy(&bd_src, &bd_dst).expect("copy bd jsonl");
let br_import = target.run_br(["sync", "--import-only"], "import_same");
let bd_import = target.run_bd(["sync", "--import-only"], "import_same");
assert!(
br_import.status.success(),
"br import with same prefix failed: {}",
br_import.stderr
);
assert!(
bd_import.status.success(),
"bd import with same prefix failed: {}",
bd_import.stderr
);
let br_list = target.run_br(["list", "--json"], "list");
let bd_list = target.run_bd(["list", "--json"], "list");
let br_val: Value = serde_json::from_str(&extract_json_payload(&br_list.stdout))
.unwrap_or(Value::Array(vec![]));
let bd_val: Value = serde_json::from_str(&extract_json_payload(&bd_list.stdout))
.unwrap_or(Value::Array(vec![]));
let br_count = br_val.as_array().map(|a| a.len()).unwrap_or(0);
let bd_count = bd_val.as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
br_count, bd_count,
"import count differs: br={}, bd={}",
br_count, bd_count
);
assert!(br_count >= 1, "should have at least 1 issue imported");
info!("conformance_sync_import_same_prefix_succeeds passed");
}
#[test]
fn conformance_sync_status_shows_prefix_info() {
skip_if_no_bd!();
common::init_test_logging();
info!("Starting conformance_sync_status_shows_prefix_info test");
let workspace = ConformanceWorkspace::new();
workspace.init_both();
workspace.run_br(["config", "--set", "id.prefix=STATUS"], "set_prefix_br");
workspace.run_br(["create", "Status test"], "create");
workspace.run_br(["sync", "--flush-only"], "flush");
let br_status = workspace.run_br(["sync", "--status", "--json"], "status");
assert!(
br_status.status.success(),
"br status failed: {}",
br_status.stderr
);
let br_json = extract_json_payload(&br_status.stdout);
let br_val: Result<Value, _> = serde_json::from_str(&br_json);
assert!(br_val.is_ok(), "br status should produce valid JSON");
info!("conformance_sync_status_shows_prefix_info passed");
}