use std::process::Command;
use super::*;
#[test]
#[ignore = "fault-injection: spawns child processes with HEDDLE_FAULT_INJECT"]
fn bridge_recovers_from_crash_after_tmp_before_commit() {
let temp = TempDir::new().unwrap();
let origin = temp.path().join("origin.git");
let work = temp.path().join("work");
let origin_repo = gix::init_bare(&origin).expect("init origin");
let blob = origin_repo.write_blob(b"fn a() {}\n").unwrap().detach();
let mut tree_editor = origin_repo
.edit_tree(origin_repo.empty_tree().id)
.expect("tree editor");
tree_editor
.upsert("core.rs", gix::object::tree::EntryKind::Blob, blob)
.unwrap();
let tree_oid = tree_editor.write().unwrap().detach();
let _commit =
git_commit_with_tree(&origin_repo, Some("refs/heads/main"), tree_oid, "seed", &[]);
heddle_output_with_env(
&["clone", origin.to_str().unwrap(), work.to_str().unwrap()],
Some(temp.path()),
&[],
)
.expect("initial clone succeeds");
let crashed = Command::new(env!("CARGO_BIN_EXE_heddle"))
.args([
"bridge",
"git",
"import",
"--path",
origin.to_str().unwrap(),
])
.current_dir(&work)
.env("HEDDLE_FAULT_INJECT", "mapping_after_tmp_before_commit")
.env("HEDDLE_CONFIG", work.join(".heddle-user/config.toml"))
.output()
.expect("spawn child");
assert!(
!crashed.status.success(),
"child should panic, got success: stdout={} stderr={}",
String::from_utf8_lossy(&crashed.stdout),
String::from_utf8_lossy(&crashed.stderr)
);
let stderr = String::from_utf8_lossy(&crashed.stderr);
assert!(
stderr.contains("HEDDLE_FAULT_INJECT")
&& stderr.contains("mapping_after_tmp_before_commit"),
"child should report the intentional panic: stderr={stderr}"
);
let mapping_dir = work.join(".heddle").join("git-bridge");
let canonical = mapping_dir.join("bridge-mapping.json");
let tmp = mapping_dir.join("bridge-mapping.json.tmp");
assert!(
tmp.exists() || canonical.exists(),
"after crash, at least one of the mapping files must exist; \
dir contents: {:?}",
std::fs::read_dir(&mapping_dir)
.map(|d| d.flatten().map(|e| e.file_name()).collect::<Vec<_>>())
.unwrap_or_default()
);
let recovered = heddle_output_with_env(
&[
"bridge",
"git",
"import",
"--path",
origin.to_str().unwrap(),
],
Some(&work),
&[],
)
.expect("recovery import succeeds");
assert!(
recovered.status.success(),
"post-crash import should succeed cleanly: stderr={}",
String::from_utf8_lossy(&recovered.stderr)
);
assert!(
canonical.exists(),
"canonical mapping file must exist after recovery"
);
let body = std::fs::read_to_string(&canonical).expect("read mapping");
let parsed: serde_json::Value =
serde_json::from_str(&body).expect("recovered mapping must parse as JSON");
assert!(
parsed.get("entries").is_some(),
"recovered mapping must have entries field: {body}"
);
assert!(
!tmp.exists(),
"post-recovery, the .tmp must be gone (renamed into canonical)"
);
}
#[test]
#[ignore = "fault-injection: spawns child processes with HEDDLE_FAULT_INJECT"]
fn snapshot_atomicity_under_simulated_sigkill() {
let temp = TempDir::new().unwrap();
heddle(&["init"], Some(temp.path())).expect("init");
std::fs::write(temp.path().join("base.txt"), "baseline").unwrap();
heddle(&["capture", "-m", "baseline"], Some(temp.path())).expect("baseline snapshot");
let log_before: Value = serde_json::from_str(
&heddle(&["--json", "log", "main", "-n", "5"], Some(temp.path())).expect("log"),
)
.unwrap();
let baseline_tip = log_before["states"][0]["change_id"]
.as_str()
.expect("baseline tip change_id")
.to_string();
std::fs::write(temp.path().join("base.txt"), "would-be-captured").unwrap();
let crashed = Command::new(env!("CARGO_BIN_EXE_heddle"))
.args(["capture", "-m", "the capture that crashes"])
.current_dir(temp.path())
.env("HEDDLE_FAULT_INJECT", "snapshot_after_state_before_ref")
.env(
"HEDDLE_CONFIG",
temp.path().join(".heddle-user/config.toml"),
)
.output()
.expect("spawn child");
assert!(
!crashed.status.success(),
"child should panic, got success: stderr={}",
String::from_utf8_lossy(&crashed.stderr)
);
let stderr = String::from_utf8_lossy(&crashed.stderr);
assert!(
stderr.contains("HEDDLE_FAULT_INJECT")
&& stderr.contains("snapshot_after_state_before_ref"),
"child should report the intentional panic: stderr={stderr}"
);
let log_after: Value = serde_json::from_str(
&heddle(&["--json", "log", "main", "-n", "5"], Some(temp.path())).expect("post-crash log"),
)
.unwrap();
let post_crash_tip = log_after["states"][0]["change_id"]
.as_str()
.expect("post-crash tip change_id");
assert_eq!(
post_crash_tip, baseline_tip,
"thread tip must still point at the baseline state — anything else \
is a half-written advance and a real atomicity bug"
);
let recovered_capture = heddle(
&["--json", "capture", "-m", "post-recovery capture"],
Some(temp.path()),
)
.expect("post-recovery capture succeeds");
let recovered: Value = serde_json::from_str(&recovered_capture).unwrap();
assert_eq!(recovered["intent"], "post-recovery capture");
let new_tip = recovered["change_id"]
.as_str()
.expect("post-recovery change_id");
assert_ne!(
new_tip, baseline_tip,
"the recovered capture must produce a fresh state, not silently \
accept the orphaned mid-crash state"
);
}