mod common;
use common::cli::{BrWorkspace, extract_json_payload, parse_created_id, run_br};
use serde_json::Value;
use std::fs;
fn show_issue(workspace: &BrWorkspace, id: &str) -> Value {
let show = run_br(
workspace,
["show", id, "--json", "--no-auto-flush", "--no-auto-import"],
"show_issue",
);
assert!(show.status.success(), "show failed: {}", show.stderr);
let mut issues: Vec<Value> =
serde_json::from_str(&extract_json_payload(&show.stdout)).expect("show JSON");
assert_eq!(issues.len(), 1, "expected exactly one issue for {id}");
issues.remove(0)
}
fn jsonl_record_for(workspace: &BrWorkspace, id: &str) -> Value {
let jsonl = fs::read_to_string(workspace.root.join(".beads").join("issues.jsonl"))
.expect("read issues.jsonl");
let line = jsonl
.lines()
.find(|line| {
serde_json::from_str::<Value>(line)
.ok()
.and_then(|record| record.get("id").and_then(Value::as_str).map(String::from))
.as_deref()
== Some(id)
})
.unwrap_or_else(|| panic!("no JSONL record for {id}; jsonl:\n{jsonl}"));
serde_json::from_str(line).expect("jsonl record")
}
#[test]
fn e2e_create_with_acceptance_criteria_and_agent_context() {
let _log = common::test_log("e2e_create_with_acceptance_criteria_and_agent_context");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let create = run_br(
&workspace,
[
"create",
"Governed issue",
"--acceptance-criteria",
"- [ ] guard refuses\n- [ ] test passes",
"--agent-context",
r#"{"workflow":"tdd","reviewer":"agent"}"#,
"--json",
"--no-auto-import",
],
"create_governed",
);
assert!(create.status.success(), "create failed: {}", create.stderr);
let created: Value =
serde_json::from_str(&extract_json_payload(&create.stdout)).expect("create JSON");
let id = created["id"].as_str().expect("created id").to_string();
assert_eq!(
created["acceptance_criteria"].as_str(),
Some("- [ ] guard refuses\n- [ ] test passes"),
"create JSON output must carry acceptance criteria"
);
let context_out = created["agent_context"]
.as_str()
.expect("agent_context in create JSON");
let context_json: Value = serde_json::from_str(context_out).expect("context is JSON");
assert_eq!(context_json["workflow"].as_str(), Some("tdd"));
let shown = show_issue(&workspace, &id);
assert_eq!(
shown["acceptance_criteria"].as_str(),
Some("- [ ] guard refuses\n- [ ] test passes")
);
let shown_context: Value =
serde_json::from_str(shown["agent_context"].as_str().expect("context")).expect("JSON");
assert_eq!(shown_context["reviewer"].as_str(), Some("agent"));
let record = jsonl_record_for(&workspace, &id);
assert_eq!(
record["acceptance_criteria"].as_str(),
Some("- [ ] guard refuses\n- [ ] test passes")
);
assert!(
record["agent_context"].as_str().is_some(),
"flushed JSONL record must carry agent_context; record: {record}"
);
}
#[test]
fn e2e_create_acceptance_alias() {
let _log = common::test_log("e2e_create_acceptance_alias");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let create = run_br(
&workspace,
[
"create",
"Alias issue",
"--acceptance",
"- [ ] alias works",
"--no-auto-flush",
"--no-auto-import",
],
"create_alias",
);
assert!(create.status.success(), "create failed: {}", create.stderr);
let id = parse_created_id(&create.stdout);
let shown = show_issue(&workspace, &id);
assert_eq!(
shown["acceptance_criteria"].as_str(),
Some("- [ ] alias works")
);
}
#[test]
fn e2e_create_agent_context_file_forms() {
let _log = common::test_log("e2e_create_agent_context_file_forms");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let json_path = workspace.root.join("ctx.json");
fs::write(&json_path, r#"{"workflow":"json-file","steps":[1,2]}"#).expect("write json");
let yaml_path = workspace.root.join("ctx.yaml");
fs::write(&yaml_path, "workflow: yaml-file\nsteps:\n - a\n - b\n").expect("write yaml");
let create_json = run_br(
&workspace,
[
"create",
"From json file",
"--agent-context",
&format!("@{}", json_path.display()),
"--no-auto-flush",
"--no-auto-import",
],
"create_ctx_json",
);
assert!(
create_json.status.success(),
"create failed: {}",
create_json.stderr
);
let id_json = parse_created_id(&create_json.stdout);
let ctx_json: Value = serde_json::from_str(
show_issue(&workspace, &id_json)["agent_context"]
.as_str()
.expect("context"),
)
.expect("JSON");
assert_eq!(ctx_json["workflow"].as_str(), Some("json-file"));
let create_yaml = run_br(
&workspace,
[
"create",
"From yaml file",
"--agent-context",
&format!("@{}", yaml_path.display()),
"--no-auto-flush",
"--no-auto-import",
],
"create_ctx_yaml",
);
assert!(
create_yaml.status.success(),
"create failed: {}",
create_yaml.stderr
);
let id_yaml = parse_created_id(&create_yaml.stdout);
let ctx_yaml: Value = serde_json::from_str(
show_issue(&workspace, &id_yaml)["agent_context"]
.as_str()
.expect("context"),
)
.expect("JSON");
assert_eq!(ctx_yaml["workflow"].as_str(), Some("yaml-file"));
assert_eq!(ctx_yaml["steps"][1].as_str(), Some("b"));
let update = run_br(
&workspace,
[
"update",
&id_json,
"--agent-context",
&format!("@{}", yaml_path.display()),
"--no-auto-flush",
"--no-auto-import",
],
"update_ctx_yaml",
);
assert!(update.status.success(), "update failed: {}", update.stderr);
assert_eq!(
show_issue(&workspace, &id_json)["agent_context"].as_str(),
show_issue(&workspace, &id_yaml)["agent_context"].as_str(),
"create and update must normalize identical YAML identically"
);
}
#[test]
fn e2e_create_invalid_agent_context_leaves_no_trace() {
let _log = common::test_log("e2e_create_invalid_agent_context_leaves_no_trace");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let create = run_br(
&workspace,
[
"create",
"Broken context",
"--agent-context",
"{not json",
"--no-auto-import",
],
"create_invalid_ctx",
);
assert!(
!create.status.success(),
"invalid agent context must fail; stdout: {}",
create.stdout
);
assert!(
create.stderr.contains("agent-context"),
"error must name the offending flag; stderr: {}",
create.stderr
);
let list = run_br(
&workspace,
[
"list",
"--all",
"--json",
"--no-auto-flush",
"--no-auto-import",
],
"list_after_invalid",
);
assert!(list.status.success(), "list failed: {}", list.stderr);
assert!(
!list.stdout.contains("Broken context"),
"no issue may exist after a rejected create; list: {}",
list.stdout
);
let jsonl =
fs::read_to_string(workspace.root.join(".beads").join("issues.jsonl")).unwrap_or_default();
assert!(
!jsonl.contains("Broken context"),
"no JSONL record may exist after a rejected create"
);
let status = run_br(
&workspace,
[
"sync",
"--status",
"--json",
"--no-auto-flush",
"--no-auto-import",
],
"sync_status_after_invalid",
);
assert!(status.status.success(), "status failed: {}", status.stderr);
let status_json: Value =
serde_json::from_str(&extract_json_payload(&status.stdout)).expect("status JSON");
assert_eq!(
status_json["dirty_issues"].as_u64().unwrap_or(0),
0,
"rejected create must not leave dirty markers; status: {status_json}"
);
}
#[test]
fn e2e_create_without_governance_flags_unchanged() {
let _log = common::test_log("e2e_create_without_governance_flags_unchanged");
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let create = run_br(
&workspace,
[
"create",
"Plain issue",
"--no-auto-flush",
"--no-auto-import",
],
"create_plain",
);
assert!(create.status.success(), "create failed: {}", create.stderr);
let id = parse_created_id(&create.stdout);
let shown = show_issue(&workspace, &id);
assert!(shown["acceptance_criteria"].is_null());
assert!(shown["agent_context"].is_null());
}