mod common;
#[cfg(feature = "self_update")]
use common::cli::parse_created_id;
use common::cli::{BrWorkspace, extract_json_payload, run_br};
use serde_json::Value;
#[cfg(feature = "self_update")]
use std::{fs, path::PathBuf};
#[cfg(feature = "self_update")]
const UPDATE_AGENT_BASELINE_ENV: &str = "UPDATE_AGENT_BASELINE";
#[test]
fn e2e_schema_json_issue() {
let _log = common::test_log("e2e_schema_json_issue");
let workspace = BrWorkspace::new();
let run = run_br(
&workspace,
["schema", "issue", "--format", "json"],
"schema_issue_json",
);
assert!(
run.status.success(),
"schema issue json failed: {}",
run.stderr
);
let payload = extract_json_payload(&run.stdout);
let json: Value = serde_json::from_str(&payload).expect("valid JSON output");
assert_eq!(json["tool"], "br");
assert!(json.get("generated_at").is_some(), "missing generated_at");
assert!(json.get("schemas").is_some(), "missing schemas");
assert!(
json["schemas"].get("Issue").is_some(),
"schemas should include Issue"
);
}
#[test]
fn e2e_schema_toon_decodes() {
let _log = common::test_log("e2e_schema_toon_decodes");
let workspace = BrWorkspace::new();
let run = run_br(
&workspace,
["schema", "issue-details", "--format", "toon"],
"schema_issue_details_toon",
);
assert!(
run.status.success(),
"schema issue-details toon failed: {}",
run.stderr
);
let toon = run.stdout.trim();
assert!(!toon.is_empty(), "TOON output should be non-empty");
let decoded = toon_rust::try_decode(toon, None).expect("valid TOON");
let json = Value::from(decoded);
assert_eq!(json["tool"], "br");
assert!(json.get("generated_at").is_some(), "missing generated_at");
let has_nested = json
.get("schemas")
.and_then(|schemas| schemas.get("IssueDetails"))
.is_some();
let has_folded = json.get("schemas.IssueDetails").is_some();
assert!(
has_nested || has_folded,
"expected IssueDetails schema (nested or folded), got keys: {:?}",
json.as_object().map(|o| o.keys().collect::<Vec<_>>())
);
}
#[cfg(feature = "self_update")]
#[test]
fn agent_baseline_snapshots_match_current_binary() {
let _log = common::test_log("agent_baseline_snapshots_match_current_binary");
let workspace = BrWorkspace::new();
compare_agent_baseline_help(&workspace);
compare_agent_baseline_schemas(&workspace);
let id_two = seed_agent_baseline_workspace(&workspace);
compare_agent_baseline_examples(&workspace, &id_two);
compare_agent_baseline_error(&workspace);
}
#[cfg(feature = "self_update")]
fn compare_agent_baseline_help(workspace: &BrWorkspace) {
compare_text_baseline(
"help/br_help.txt",
&run_success(workspace, ["--help"], "baseline_help"),
);
compare_text_baseline(
"help/br_list_help.txt",
&run_success(workspace, ["list", "--help"], "baseline_list_help"),
);
compare_text_baseline(
"help/br_schema_help.txt",
&run_success(workspace, ["schema", "--help"], "baseline_schema_help"),
);
}
#[cfg(feature = "self_update")]
fn compare_agent_baseline_schemas(workspace: &BrWorkspace) {
compare_json_baseline(
"schemas/schema_all.json",
&run_success(
workspace,
["schema", "all", "--format", "json"],
"baseline_schema_all",
),
normalize_schema_snapshot,
);
compare_json_baseline(
"schemas/schema_error.json",
&run_success(
workspace,
["schema", "error", "--format", "json"],
"baseline_schema_error",
),
normalize_schema_snapshot,
);
compare_json_baseline(
"schemas/schema_issue_details.json",
&run_success(
workspace,
["schema", "issue-details", "--format", "json"],
"baseline_schema_issue_details",
),
normalize_schema_snapshot,
);
}
#[cfg(feature = "self_update")]
fn seed_agent_baseline_workspace(workspace: &BrWorkspace) -> String {
let init = run_br(workspace, ["init", "--prefix", "bd"], "baseline_init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let create_one = run_br(
workspace,
[
"create",
"One",
"--type",
"task",
"--priority",
"2",
"--description",
"Short desc",
],
"baseline_create_one",
);
assert!(
create_one.status.success(),
"create one failed: {}",
create_one.stderr
);
let create_two = run_br(
workspace,
["create", "Two", "--type", "bug", "--priority", "0"],
"baseline_create_two",
);
assert!(
create_two.status.success(),
"create two failed: {}",
create_two.stderr
);
let id_two = parse_created_id(&create_two.stdout);
let create_three = run_br(
workspace,
["create", "Three", "--type", "feature", "--priority", "1"],
"baseline_create_three",
);
assert!(
create_three.status.success(),
"create three failed: {}",
create_three.stderr
);
id_two
}
#[cfg(feature = "self_update")]
fn compare_agent_baseline_examples(workspace: &BrWorkspace, id_two: &str) {
compare_json_baseline(
"examples/list_limit3.json",
&run_success(
workspace,
["list", "--format", "json", "--limit", "3"],
"baseline_list_limit3_json",
),
normalize_issue_example_snapshot,
);
compare_toon_baseline(
"examples/list_limit3.toon",
&run_success(
workspace,
["list", "--format", "toon", "--limit", "3"],
"baseline_list_limit3_toon",
),
);
compare_json_baseline(
"examples/ready.json",
&run_success(
workspace,
["ready", "--format", "json"],
"baseline_ready_json",
),
normalize_issue_example_snapshot,
);
compare_toon_baseline(
"examples/ready.toon",
&run_success(
workspace,
["ready", "--format", "toon"],
"baseline_ready_toon",
),
);
compare_json_baseline(
"examples/show_one.json",
&run_success(
workspace,
["show", id_two, "--format", "json"],
"baseline_show_one_json",
),
normalize_issue_example_snapshot,
);
compare_toon_baseline(
"examples/show_one.toon",
&run_success(
workspace,
["show", id_two, "--format", "toon"],
"baseline_show_one_toon",
),
);
compare_json_baseline(
"examples/version.json",
&run_success(workspace, ["version", "--json"], "baseline_version_json"),
normalize_version_snapshot,
);
}
#[cfg(feature = "self_update")]
fn compare_agent_baseline_error(workspace: &BrWorkspace) {
let missing = run_br(
workspace,
["show", "bd-NOTEXIST", "--json"],
"baseline_show_not_found",
);
assert_eq!(
missing.status.code(),
Some(3),
"unexpected status: {missing:?}"
);
compare_json_baseline(
"errors/show_not_found.json",
&missing.stderr,
normalize_noop,
);
}
#[cfg(feature = "self_update")]
fn run_success<const N: usize>(workspace: &BrWorkspace, args: [&str; N], label: &str) -> String {
let run = run_br(workspace, args, label);
assert!(
run.status.success(),
"{label} failed: stdout={} stderr={}",
run.stdout,
run.stderr
);
run.stdout
}
#[cfg(feature = "self_update")]
fn compare_text_baseline(relative_path: &str, actual: &str) {
let path = baseline_path(relative_path);
let actual = normalize_text_snapshot(actual);
if should_update_agent_baseline() {
fs::write(&path, actual).expect("update agent baseline text snapshot");
return;
}
let expected = fs::read_to_string(&path).expect("read agent baseline text snapshot");
let expected = normalize_text_snapshot(&expected);
assert_eq!(
expected, actual,
"agent_baseline/{relative_path} is stale; rerun with {UPDATE_AGENT_BASELINE_ENV}=1"
);
}
#[cfg(feature = "self_update")]
fn compare_json_baseline(relative_path: &str, actual: &str, normalize: fn(&mut Value)) {
let path = baseline_path(relative_path);
let actual_payload = extract_json_payload(actual);
if should_update_agent_baseline() {
fs::write(&path, with_trailing_newline(&actual_payload))
.expect("update agent baseline JSON snapshot");
return;
}
let expected_raw = fs::read_to_string(&path).expect("read agent baseline JSON snapshot");
let mut expected: Value =
serde_json::from_str(&expected_raw).expect("valid agent baseline JSON snapshot");
let mut actual: Value =
serde_json::from_str(&actual_payload).expect("valid generated JSON for agent baseline");
normalize(&mut expected);
normalize(&mut actual);
assert_eq!(
expected, actual,
"agent_baseline/{relative_path} is stale; rerun with {UPDATE_AGENT_BASELINE_ENV}=1"
);
}
#[cfg(feature = "self_update")]
fn compare_toon_baseline(relative_path: &str, actual: &str) {
let path = baseline_path(relative_path);
let actual = with_trailing_newline(actual.trim_end());
if should_update_agent_baseline() {
fs::write(&path, actual).expect("update agent baseline TOON snapshot");
return;
}
let expected_raw = fs::read_to_string(&path).expect("read agent baseline TOON snapshot");
let mut expected = Value::from(
toon_rust::try_decode(&expected_raw, None).expect("valid agent baseline TOON snapshot"),
);
let mut actual = Value::from(
toon_rust::try_decode(&actual, None).expect("valid generated TOON for agent baseline"),
);
normalize_issue_example_snapshot(&mut expected);
normalize_issue_example_snapshot(&mut actual);
assert_eq!(
expected, actual,
"agent_baseline/{relative_path} is stale; rerun with {UPDATE_AGENT_BASELINE_ENV}=1"
);
}
#[cfg(feature = "self_update")]
fn baseline_path(relative_path: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("agent_baseline")
.join(relative_path)
}
#[cfg(feature = "self_update")]
fn should_update_agent_baseline() -> bool {
std::env::var_os(UPDATE_AGENT_BASELINE_ENV).is_some()
}
#[cfg(feature = "self_update")]
fn with_trailing_newline(text: &str) -> String {
format!("{text}\n")
}
#[cfg(feature = "self_update")]
fn normalize_text_snapshot(text: &str) -> String {
let mut normalized = text
.lines()
.map(str::trim_end)
.collect::<Vec<_>>()
.join("\n");
normalized.push('\n');
normalized
}
#[cfg(feature = "self_update")]
fn normalize_schema_snapshot(value: &mut Value) {
if let Some(object) = value.as_object_mut() {
object.insert(
"generated_at".to_string(),
Value::String("<GENERATED_AT>".to_string()),
);
}
}
#[cfg(feature = "self_update")]
fn normalize_version_snapshot(value: &mut Value) {
if let Some(object) = value.as_object_mut() {
for key in ["branch", "build", "commit", "rust_version", "target"] {
if object.contains_key(key) {
object.insert(key.to_string(), Value::String(format!("<{key}>")));
}
}
}
}
#[cfg(feature = "self_update")]
fn normalize_issue_example_snapshot(value: &mut Value) {
match value {
Value::Array(items) => {
for item in items {
normalize_issue_example_snapshot(item);
}
}
Value::Object(object) => {
for (key, child) in object {
match key.as_str() {
"closed_at" | "created_at" | "updated_at" => {
*child = Value::String("<TIMESTAMP>".to_string());
}
"created_by" => {
*child = Value::String("<ACTOR>".to_string());
}
"depends_on_id" | "id" | "issue_id" => {
*child = Value::String("<ISSUE_ID>".to_string());
}
_ => normalize_issue_example_snapshot(child),
}
}
}
Value::Bool(_) | Value::Null | Value::Number(_) | Value::String(_) => {}
}
}
#[cfg(feature = "self_update")]
fn normalize_noop(_: &mut Value) {}