mod common;
use common::cli::{BrWorkspace, extract_json_payload, run_br};
use serde_json::Value;
#[test]
fn e2e_version_shows_version() {
let workspace = BrWorkspace::new();
let version = run_br(&workspace, ["version"], "version_basic");
assert!(
version.status.success(),
"version command failed: {}",
version.stderr
);
assert!(
version.stdout.contains("br version"),
"output should contain 'br version', got: {}",
version.stdout
);
}
#[test]
fn e2e_version_json_output() {
let workspace = BrWorkspace::new();
let version = run_br(&workspace, ["version", "--json"], "version_json");
assert!(
version.status.success(),
"version --json failed: {}",
version.stderr
);
let json_str = extract_json_payload(&version.stdout);
let json: Value = serde_json::from_str(&json_str).expect("valid JSON");
assert!(json.get("version").is_some(), "missing 'version' field");
assert!(json.get("build").is_some(), "missing 'build' field");
assert!(json.get("commit").is_some(), "missing 'commit' field");
assert!(json.get("branch").is_some(), "missing 'branch' field");
}
#[test]
fn e2e_version_no_workspace_required() {
let workspace = BrWorkspace::new();
let version = run_br(&workspace, ["version"], "version_no_workspace");
assert!(
version.status.success(),
"version should work without workspace: {}",
version.stderr
);
}
#[test]
fn e2e_version_quiet_suppresses_output() {
let workspace = BrWorkspace::new();
let version = run_br(&workspace, ["--quiet", "version"], "version_quiet");
assert!(
version.status.success(),
"quiet version command failed: {}",
version.stderr
);
assert!(
version.stdout.trim().is_empty(),
"quiet version should not print"
);
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_check_attempts_api_call() {
let workspace = BrWorkspace::new();
let upgrade = run_br(&workspace, ["upgrade", "--check"], "upgrade_check");
assert!(
upgrade.stdout.contains("version")
|| upgrade.stdout.contains("error")
|| upgrade.stderr.contains("error")
|| upgrade.stderr.contains("NetworkError"),
"upgrade --check should output version or error info"
);
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_check_json_error_structure() {
let workspace = BrWorkspace::new();
let upgrade = run_br(
&workspace,
["upgrade", "--check", "--json"],
"upgrade_check_json",
);
let output = if upgrade.stdout.trim().is_empty() {
&upgrade.stderr
} else {
&upgrade.stdout
};
let json_str = extract_json_payload(output);
if !json_str.is_empty() {
let result: Result<Value, _> = serde_json::from_str(&json_str);
assert!(
result.is_ok(),
"output should be valid JSON, got: {json_str}"
);
}
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_dry_run_no_changes() {
let workspace = BrWorkspace::new();
let upgrade = run_br(&workspace, ["upgrade", "--dry-run"], "upgrade_dry_run");
assert!(
upgrade.stdout.contains("dry-run")
|| upgrade.stdout.contains("Dry-run")
|| upgrade.stdout.contains("would")
|| upgrade.stderr.contains("dry-run")
|| upgrade.stderr.contains("Dry-run")
|| upgrade.stderr.contains("NetworkError"),
"dry-run should indicate it's a dry run or show network error"
);
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_dry_run_json() {
let workspace = BrWorkspace::new();
let upgrade = run_br(
&workspace,
["upgrade", "--dry-run", "--json"],
"upgrade_dry_run_json",
);
let output = if upgrade.stdout.trim().is_empty() {
&upgrade.stderr
} else {
&upgrade.stdout
};
let json_str = extract_json_payload(output);
if !json_str.is_empty() {
let result: Result<Value, _> = serde_json::from_str(&json_str);
assert!(
result.is_ok(),
"output should be valid JSON, got: {json_str}"
);
}
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_check_quiet_suppresses_output() {
let workspace = BrWorkspace::new();
let upgrade = run_br(
&workspace,
["--quiet", "upgrade", "--check"],
"upgrade_check_quiet",
);
assert!(
upgrade.stdout.trim().is_empty(),
"quiet upgrade --check should not print stdout: {}",
upgrade.stdout
);
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_dry_run_quiet_suppresses_output() {
let workspace = BrWorkspace::new();
let upgrade = run_br(
&workspace,
["--quiet", "upgrade", "--dry-run"],
"upgrade_dry_run_quiet",
);
assert!(
upgrade.stdout.trim().is_empty(),
"quiet upgrade --dry-run should not print stdout: {}",
upgrade.stdout
);
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_with_version_flag() {
let workspace = BrWorkspace::new();
let upgrade = run_br(
&workspace,
["upgrade", "--version", "0.1.0", "--dry-run"],
"upgrade_specific_version",
);
assert!(
upgrade.stdout.contains("0.1.0")
|| upgrade.stderr.contains("0.1.0")
|| upgrade.stderr.contains("NetworkError")
|| upgrade.stdout.contains("error"),
"should reference version or show network error"
);
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_force_flag_accepted() {
let workspace = BrWorkspace::new();
let upgrade = run_br(
&workspace,
["upgrade", "--force", "--dry-run"],
"upgrade_force",
);
assert!(
!upgrade.stderr.contains("unknown argument") && !upgrade.stderr.contains("unrecognized"),
"--force should be a valid argument"
);
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_graceful_network_error() {
let workspace = BrWorkspace::new();
let upgrade = run_br(
&workspace,
["upgrade", "--check", "--json"],
"upgrade_network_error",
);
if !upgrade.status.success() {
let output = if upgrade.stdout.trim().is_empty() {
&upgrade.stderr
} else {
&upgrade.stdout
};
let json_str = extract_json_payload(output);
if !json_str.is_empty() {
let json: Result<Value, _> = serde_json::from_str(&json_str);
if let Ok(json) = json {
if json.get("error").is_some() {
let error = &json["error"];
assert!(
error.get("message").is_some() || error.get("code").is_some(),
"error should have message or code"
);
}
}
}
}
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_no_workspace_required() {
let workspace = BrWorkspace::new();
let upgrade = run_br(&workspace, ["upgrade", "--check"], "upgrade_no_workspace");
assert!(
!upgrade.stderr.contains("No .beads") && !upgrade.stderr.contains("not initialized"),
"upgrade should not require workspace initialization"
);
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_check_with_force_error() {
let workspace = BrWorkspace::new();
let upgrade = run_br(
&workspace,
["upgrade", "--check", "--force"],
"upgrade_check_force",
);
assert!(
upgrade.status.success()
|| upgrade.stderr.contains("conflict")
|| upgrade.stderr.contains("NetworkError")
|| upgrade.stdout.contains("error"),
"conflicting flags should be handled"
);
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_help_works() {
let workspace = BrWorkspace::new();
let upgrade = run_br(&workspace, ["upgrade", "--help"], "upgrade_help");
assert!(
upgrade.status.success(),
"upgrade --help failed: {}",
upgrade.stderr
);
assert!(
upgrade.stdout.contains("--check") && upgrade.stdout.contains("--dry-run"),
"help should mention available flags"
);
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_feature_enabled() {
let workspace = BrWorkspace::new();
let upgrade = run_br(&workspace, ["upgrade", "--help"], "upgrade_feature_check");
let output_combined = format!("{}{}", upgrade.stdout, upgrade.stderr);
let feature_enabled = !output_combined.contains("unrecognized subcommand")
&& !output_combined.contains("unknown command")
&& !output_combined.contains("invalid subcommand");
if !feature_enabled {
eprintln!(
"Note: self_update feature appears to be disabled. Upgrade tests will skip gracefully."
);
}
}
#[cfg(feature = "self_update")]
fn full_upgrade_tests_enabled() -> bool {
std::env::var("BR_TEST_FULL_UPGRADE").is_ok_and(|v| v == "1" || v.to_lowercase() == "true")
}
#[cfg(feature = "self_update")]
fn setup_isolated_binary(workspace: &BrWorkspace) -> Option<std::path::PathBuf> {
let bin_dir = workspace.root.join("bin");
std::fs::create_dir_all(&bin_dir).ok()?;
let target_binary = bin_dir.join("br");
let current_binary = assert_cmd::cargo::cargo_bin!("br");
std::fs::copy(current_binary, &target_binary).ok()?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&target_binary).ok()?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&target_binary, perms).ok()?;
}
Some(target_binary)
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_guarded_full_upgrade_skipped_without_env() {
if full_upgrade_tests_enabled() {
return;
}
eprintln!("Full upgrade tests are disabled. Set BR_TEST_FULL_UPGRADE=1 to enable.");
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_guarded_isolated_binary_setup() {
if !full_upgrade_tests_enabled() {
eprintln!("Skipping: BR_TEST_FULL_UPGRADE not set");
return;
}
let workspace = BrWorkspace::new();
let isolated_binary = setup_isolated_binary(&workspace);
assert!(
isolated_binary.is_some(),
"should be able to copy binary to isolated location"
);
let binary_path = isolated_binary.unwrap();
assert!(binary_path.exists(), "isolated binary should exist");
let output = std::process::Command::new(&binary_path)
.arg("version")
.output()
.expect("run isolated binary");
assert!(
output.status.success(),
"isolated binary should run: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_guarded_full_upgrade_check_only() {
if !full_upgrade_tests_enabled() {
eprintln!("Skipping: BR_TEST_FULL_UPGRADE not set");
return;
}
let workspace = BrWorkspace::new();
let isolated_binary = setup_isolated_binary(&workspace);
if isolated_binary.is_none() {
eprintln!("Skipping: could not set up isolated binary");
return;
}
let binary_path = isolated_binary.unwrap();
let output = std::process::Command::new(&binary_path)
.args(["upgrade", "--check", "--json"])
.current_dir(&workspace.root)
.output()
.expect("run upgrade --check");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stdout.contains("version") || stdout.contains("error") || stderr.contains("NetworkError"),
"upgrade --check should return version info or error"
);
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_guarded_dry_run_isolated() {
if !full_upgrade_tests_enabled() {
eprintln!("Skipping: BR_TEST_FULL_UPGRADE not set");
return;
}
let workspace = BrWorkspace::new();
let isolated_binary = setup_isolated_binary(&workspace);
if isolated_binary.is_none() {
eprintln!("Skipping: could not set up isolated binary");
return;
}
let binary_path = isolated_binary.unwrap();
let output = std::process::Command::new(&binary_path)
.args(["upgrade", "--dry-run", "--json"])
.current_dir(&workspace.root)
.output()
.expect("run upgrade --dry-run");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stdout.contains("dry_run")
|| stdout.contains("would")
|| stderr.contains("NetworkError")
|| stderr.contains("error"),
"dry-run should indicate no changes: stdout={stdout}, stderr={stderr}"
);
let binary_exists = binary_path.exists();
assert!(
binary_exists,
"isolated binary should still exist after dry-run"
);
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_captures_network_error_in_log() {
let workspace = BrWorkspace::new();
let upgrade = run_br(
&workspace,
["upgrade", "--check", "--json"],
"upgrade_network_log",
);
let log_content = std::fs::read_to_string(&upgrade.log_path).unwrap_or_default();
assert!(
log_content.contains("upgrade"),
"log should contain command name"
);
if !upgrade.status.success() {
assert!(
log_content.contains("stderr") || log_content.contains("error"),
"log should capture error output"
);
}
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_json_error_is_valid_json() {
let workspace = BrWorkspace::new();
let upgrade = run_br(
&workspace,
["upgrade", "--check", "--json"],
"upgrade_json_valid",
);
let output = if upgrade.stdout.trim().is_empty() {
&upgrade.stderr
} else {
&upgrade.stdout
};
let json_str = extract_json_payload(output);
if !json_str.is_empty() {
let parse_result: Result<serde_json::Value, _> = serde_json::from_str(&json_str);
assert!(
parse_result.is_ok(),
"JSON output should be valid: {} (error: {:?})",
json_str,
parse_result.err()
);
}
}
#[cfg(feature = "self_update")]
#[test]
fn e2e_upgrade_consistent_help_output() {
let workspace = BrWorkspace::new();
let run1 = run_br(&workspace, ["upgrade", "--help"], "upgrade_help_1");
let run2 = run_br(&workspace, ["upgrade", "--help"], "upgrade_help_2");
assert!(run1.status.success(), "run1 failed");
assert!(run2.status.success(), "run2 failed");
assert_eq!(
run1.stdout, run2.stdout,
"help output should be consistent across runs"
);
}
#[test]
fn e2e_upgrade_version_output_stable() {
let workspace = BrWorkspace::new();
let run1 = run_br(&workspace, ["version"], "version_stable_1");
let run2 = run_br(&workspace, ["version"], "version_stable_2");
assert!(run1.status.success(), "run1 failed");
assert!(run2.status.success(), "run2 failed");
assert_eq!(
run1.stdout, run2.stdout,
"version output should be stable across runs"
);
}