use super::*;
use std::cell::RefCell;
use std::path::Path;
#[derive(Default)]
struct FakeRepo {
calls: RefCell<Vec<String>>,
fail_integration: bool,
}
impl FakeRepo {
fn log(&self, call: String) {
self.calls.borrow_mut().push(call);
}
fn calls(&self) -> Vec<String> {
self.calls.borrow().clone()
}
}
impl Repo for FakeRepo {
fn materialize(&self, path: &Path, branch: &str) -> io::Result<()> {
self.log(format!("materialize {} {branch}", path.display()));
Ok(())
}
fn release(&self, path: &Path) -> io::Result<()> {
self.log(format!("release {}", path.display()));
Ok(())
}
fn discard(&self, path: &Path, branch: &str) -> io::Result<()> {
self.log(format!("discard {} {branch}", path.display()));
Ok(())
}
fn integration(&self) -> io::Result<String> {
if self.fail_integration {
return Err(io::Error::other("no integration branch"));
}
self.log("integration".into());
Ok("main".into())
}
fn mint(&self, branch: &str, base: &str) -> io::Result<()> {
self.log(format!("mint {branch} at {base}"));
Ok(())
}
fn deliver(&self, path: &Path, branch: &str, integration: &str, message: &str, marker: &str) -> io::Result<()> {
self.log(format!("deliver {} {branch} -> {integration} : {message} : {marker}", path.display()));
Ok(())
}
fn work_messages(&self, _branch: &str, _integration: &str) -> io::Result<Vec<String>> {
Ok(Vec::new()) }
fn is_git_repo(&self) -> io::Result<bool> {
unreachable!("dispatch never gates on the precondition (see delivery_precondition)")
}
}
fn spec() -> Spec<'static> {
targeted(None)
}
fn targeted(target: Option<&'static str>) -> Spec<'static> {
Spec {
worktree: Path::new("/wt"),
branch: "work/bl-f813",
subject: "Title [bl-f813]",
override_msg: None,
marker: "[bl-f813]",
target,
}
}
fn drive(op: &str, phase: &str, rolling_back: bool) -> Vec<String> {
drive_spec(op, phase, rolling_back, &spec())
}
fn drive_spec(op: &str, phase: &str, rolling_back: bool, spec: &Spec) -> Vec<String> {
let repo = FakeRepo::default();
dispatch(op, phase, rolling_back, &repo, spec).unwrap();
repo.calls()
}
#[test]
fn claim_post_materializes() {
assert_eq!(drive("claim", "post", false), ["materialize /wt work/bl-f813"]);
}
#[test]
fn prime_post_does_not_materialize() {
assert_eq!(drive("prime", "post", false), Vec::<String>::new());
}
#[test]
fn unclaim_post_releases() {
assert_eq!(drive("unclaim", "post", false), ["release /wt"]);
}
#[test]
fn close_pre_resolves_integration_then_delivers() {
assert_eq!(
drive("close", "pre", false),
["integration", "deliver /wt work/bl-f813 -> main : Title [bl-f813] : [bl-f813]"]
);
}
#[test]
fn close_post_releases() {
assert_eq!(drive("close", "post", false), ["release /wt"]);
}
#[test]
fn claim_post_rollback_discards_worktree_and_branch() {
assert_eq!(drive("claim", "post", true), ["discard /wt work/bl-f813"]);
}
#[test]
fn declining_rollbacks_and_unwired_hooks_are_noops() {
assert!(drive("close", "pre", true).is_empty());
assert!(drive("close", "post", true).is_empty()); assert!(drive("unclaim", "post", true).is_empty()); assert!(drive("create", "post", false).is_empty()); assert!(drive("claim", "pre", false).is_empty()); }
#[test]
fn a_nested_claim_mints_the_targets_ref_then_forks_its_own_off_it() {
assert_eq!(
drive_spec("claim", "post", false, &targeted(Some("bl-epic"))),
[
"integration",
"mint work/bl-epic at main",
"mint work/bl-f813 at work/bl-epic",
"materialize /wt work/bl-f813",
]
);
}
#[test]
fn a_nested_close_delivers_into_the_targets_ref_not_the_integration_branch() {
assert_eq!(
drive_spec("close", "pre", false, &targeted(Some("bl-epic"))),
[
"integration",
"mint work/bl-epic at main",
"deliver /wt work/bl-f813 -> work/bl-epic : Title [bl-f813] : [bl-f813]",
]
);
}
#[test]
fn a_flat_claim_forks_head_and_mints_nothing() {
assert_eq!(drive("claim", "post", false), ["materialize /wt work/bl-f813"]);
}
#[test]
fn an_integration_failure_aborts_a_close() {
let repo = FakeRepo { fail_integration: true, ..FakeRepo::default() };
assert!(dispatch("close", "pre", false, &repo, &spec()).is_err());
}
#[test]
fn resolve_id_prefers_the_sealed_metadata_trailer() {
let mut md = Metadata::new();
md.insert("bl-id".into(), vec!["bl-abc1".into()]);
assert_eq!(resolve_id(Some(&md), None).unwrap(), "bl-abc1");
}
#[test]
fn resolve_id_takes_the_ball_off_the_pre_wires_command() {
assert_eq!(resolve_id(None, Some("bl-9f9f")).unwrap(), "bl-9f9f");
}
#[test]
fn resolve_id_rejects_a_wire_that_names_no_ball() {
let err = resolve_id(None, None).unwrap_err();
assert!(err.to_string().contains("no ball on the wire"), "{err}");
}
#[test]
fn claim_post_surfaces_the_bare_path() {
let wt = Path::new("/wt/bl-x");
assert_eq!(surfaced("claim", "post", false, wt, true).as_deref(), Some("/wt/bl-x"));
}
#[test]
fn show_read_surfaces_a_field_line_only_when_the_worktree_exists() {
let wt = Path::new("/wt/bl-x");
assert_eq!(surfaced("show", "read", false, wt, true).as_deref(), Some(" worktree /wt/bl-x"));
assert_eq!(surfaced("show", "read", false, wt, false), None);
}
#[test]
fn no_other_hook_or_rollback_surfaces_anything() {
for (op, phase, rb, exists) in [
("claim", "post", true, true), ("claim", "pre", false, true),
("prime", "post", false, true), ("unclaim", "post", false, true),
("close", "pre", false, true),
("show", "read", true, true), ] {
assert_eq!(surfaced(op, phase, rb, Path::new("/wt"), exists), None, "{op}.{phase} rb={rb}");
}
}
#[test]
fn protocol_self_description_lists_every_hooked_op() {
let v: serde_json::Value = serde_json::from_str(PROTOCOL_JSON).unwrap();
assert_eq!(v["protocol"], serde_json::json!([1]));
assert_eq!(v["ops"], serde_json::json!(["claim", "unclaim", "close", "prime", "show"]));
}