use super::*;
#[derive(Debug, serde::Deserialize)]
struct RealworldRegistry {
repo: Vec<RealworldRepo>,
}
#[derive(Debug, serde::Deserialize)]
struct RealworldRepo {
name: String,
source: String,
fixture: String,
commit: String,
shape: Vec<String>,
}
fn heddle_with_host_git(args: &[&str], cwd: &std::path::Path) -> Result<String, String> {
super::heddle(args, Some(cwd))
}
fn heddle_without_git(args: &[&str], cwd: &std::path::Path) -> Result<String, String> {
let output = heddle_output_with_env(args, Some(cwd), &[("PATH", "")])?;
let stdout = str::from_utf8(&output.stdout).unwrap_or("").to_string();
let stderr = str::from_utf8(&output.stderr).unwrap_or("").to_string();
if output.status.success() {
Ok(stdout)
} else {
Err(format!(
"Exit code: {:?}\nstdout: {}\nstderr: {}",
output.status.code(),
stdout,
stderr
))
}
}
fn registry() -> RealworldRegistry {
let raw = include_str!("../realworld_git/realworld_repos.toml");
toml::from_str(raw).expect("realworld repo registry should parse")
}
fn extract_fixture(name: &str) -> (TempDir, std::path::PathBuf) {
let registry = registry();
let entry = registry
.repo
.iter()
.find(|r| r.name == name)
.unwrap_or_else(|| panic!("registry missing fixture: {name}"));
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let tarball = manifest_dir
.join("tests")
.join("realworld_git")
.join(&entry.fixture);
assert!(
tarball.exists(),
"vendored fixture missing: {} (run `crates/cli/tests/realworld_git/fixtures/vendor.sh`)",
tarball.display()
);
let temp = TempDir::new().expect("temp dir for fixture extract");
let status = std::process::Command::new("tar")
.args(["xzf", tarball.to_str().unwrap()])
.current_dir(temp.path())
.status()
.expect("tar invocation");
assert!(status.success(), "tar xzf failed for {}", tarball.display());
let bare = temp.path().join(name);
assert!(
bare.join("HEAD").exists(),
"bare repo missing HEAD after extract: {}",
bare.display()
);
let extracted = gix::open(&bare).expect("open extracted bare repo");
let head = extracted
.head_commit()
.expect("extracted fixture should resolve HEAD");
assert_eq!(
head.id().to_string(),
entry.commit,
"fixture {name} drifted: registry pinned {} but extracted tip is {}",
entry.commit,
head.id()
);
(temp, bare)
}
fn git_tree_with_file(repo: &gix::Repository, path: &str, content: &[u8]) -> gix::hash::ObjectId {
let blob = repo.write_blob(content).expect("write git blob").detach();
let empty = git_empty_tree_oid(repo);
let mut editor = repo.edit_tree(empty).expect("edit git tree");
editor
.upsert(path, gix::object::tree::EntryKind::Blob, blob)
.expect("add file to git tree");
editor.write().expect("write git tree").detach()
}
#[test]
fn realworld_git_fixture_registry_is_parseable_and_pinned() {
let registry = registry();
assert_eq!(registry.repo.len(), 4);
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
for repo in registry.repo {
assert!(!repo.name.is_empty(), "registry entries need stable names");
assert!(
repo.fixture.starts_with("fixtures/") && repo.fixture.ends_with(".tar.gz"),
"fixture should be a vendored tarball under fixtures/: {repo:?}"
);
assert_eq!(
repo.commit.len(),
40,
"fixture commit pin must be a real 40-char SHA-1: {repo:?}"
);
assert!(
repo.commit
.chars()
.all(|c| matches!(c, '0'..='9' | 'a'..='f')),
"fixture commit pin must be lowercase hex: {repo:?}"
);
assert!(
repo.source.starts_with("https://github.com/"),
"source should document the public repo: {repo:?}"
);
assert!(!repo.shape.is_empty(), "shape tags drive matrix coverage");
let tarball = manifest_dir
.join("tests")
.join("realworld_git")
.join(&repo.fixture);
assert!(
tarball.exists(),
"registry references missing tarball: {} (run vendor.sh to create)",
tarball.display()
);
}
}
#[test]
#[ignore = "nightly real-world matrix: generates complex overlay fixtures"]
fn realworld_git_complex_fixture_round_trips_overlay_inventory_without_git_on_path() {
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 synthetic real-world origin");
let base_tree = git_tree_with_file(&origin_repo, "core.rs", b"pub fn base() {}\n");
let base = git_commit_with_tree(
&origin_repo,
Some("refs/heads/main"),
base_tree,
"base",
&[],
);
git_set_reference(&origin_repo, "HEAD", base);
let feature_tree = git_tree_with_file(
&origin_repo,
"core.rs",
b"pub fn base() {}\npub fn feature() {}\n",
);
let feature = git_commit_with_tree(
&origin_repo,
Some("refs/heads/feature/parser"),
feature_tree,
"feature parser",
&[base],
);
let docs_tree = git_tree_with_file(&origin_repo, "guide.md", b"# Guide\n");
let docs = git_commit_with_tree(
&origin_repo,
Some("refs/heads/feature/docs"),
docs_tree,
"feature docs",
&[base],
);
let merge_tree = git_tree_with_file(
&origin_repo,
"core.rs",
b"pub fn base() {}\npub fn feature() {}\npub fn docs() {}\n",
);
let merge = git_commit_with_tree(
&origin_repo,
Some("refs/heads/main"),
merge_tree,
"octopus-shaped merge",
&[base, feature, docs],
);
git_set_reference(&origin_repo, "refs/tags/v0.1.0", merge);
heddle_without_git(
&[
"clone",
origin.to_str().expect("origin path should be utf8"),
work.to_str().expect("work path should be utf8"),
],
temp.path(),
)
.unwrap();
heddle_without_git(&["bridge", "import"], &work).unwrap();
let fsck = heddle_with_host_git(&["fsck", "--bridge", "--json"], &work).unwrap();
let parsed: Value = serde_json::from_str(&fsck).expect("fsck output should parse");
assert_eq!(parsed["valid"], true, "complex fixture should fsck: {fsck}");
let threads = heddle_with_host_git(&["thread", "list", "--json"], &work).unwrap();
assert!(
threads.contains("feature/parser") || threads.contains("feature-docs"),
"import should expose non-main refs as overlay threads: {threads}"
);
}
#[test]
#[ignore = "stress fixture: set HEDDLE_LARGE_BLOB_MB=200 (≤256 cap) to exercise the release budget"]
fn realworld_git_large_binary_blob_stress_without_git_on_path() {
let size_mb: usize = std::env::var("HEDDLE_LARGE_BLOB_MB")
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(200);
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 large origin");
let large = vec![0xA5; size_mb * 1024 * 1024];
let tree = git_tree_with_file(&origin_repo, "large.bin", &large);
let commit = git_commit_with_tree(
&origin_repo,
Some("refs/heads/main"),
tree,
"large binary",
&[],
);
git_set_reference(&origin_repo, "HEAD", commit);
heddle_without_git(
&[
"clone",
origin.to_str().expect("origin path should be utf8"),
work.to_str().expect("work path should be utf8"),
],
temp.path(),
)
.unwrap();
let metadata = std::fs::metadata(work.join("large.bin")).unwrap();
assert!(
metadata.len() > 0,
"large checkout should materialize a blob or a safety pointer"
);
let fsck = heddle_with_host_git(&["fsck", "--bridge", "--json"], &work).unwrap();
let parsed: Value = serde_json::from_str(&fsck).expect("fsck output should parse");
assert_eq!(parsed["valid"], true, "large fixture should fsck: {fsck}");
}
#[test]
#[ignore = "nightly real-world matrix: rebase round-trip"]
fn realworld_git_rebase_chain_round_trips_overlay() {
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 base_tree = git_tree_with_file(&origin_repo, "core.rs", b"fn base() {}\n");
let base = git_commit_with_tree(
&origin_repo,
Some("refs/heads/main"),
base_tree,
"base",
&[],
);
git_set_reference(&origin_repo, "HEAD", base);
let mut prev = base;
let mut commits = Vec::new();
for i in 0..5 {
let tree = git_tree_with_file(
&origin_repo,
"core.rs",
format!("fn base() {{}}\n// step {i}\n").as_bytes(),
);
prev = git_commit_with_tree(
&origin_repo,
Some("refs/heads/feature/chain"),
tree,
&format!("step {i}"),
&[prev],
);
commits.push(prev);
}
let new_base_tree = git_tree_with_file(&origin_repo, "core.rs", b"fn rebased_base() {}\n");
let new_base = git_commit_with_tree(
&origin_repo,
Some("refs/heads/main"),
new_base_tree,
"advance main",
&[base],
);
let mut rebased_prev = new_base;
let mut rebased_commits = Vec::new();
for i in 0..5 {
let tree = git_tree_with_file(
&origin_repo,
"core.rs",
format!("fn rebased_base() {{}}\n// step {i}\n").as_bytes(),
);
rebased_prev = git_commit_with_tree(
&origin_repo,
Some("refs/heads/feature/chain"),
tree,
&format!("step {i}"),
&[rebased_prev],
);
rebased_commits.push(rebased_prev);
}
heddle_without_git(
&["clone", origin.to_str().unwrap(), work.to_str().unwrap()],
temp.path(),
)
.unwrap();
heddle_without_git(&["bridge", "import"], &work).unwrap();
let threads = serde_json::from_str::<Value>(
&heddle_with_host_git(&["thread", "list", "--json"], &work).unwrap(),
)
.unwrap();
let names: Vec<String> = threads["threads"]
.as_array()
.unwrap()
.iter()
.map(|t| t["name"].as_str().unwrap().to_string())
.collect();
assert!(
names.iter().any(|n| n == "feature/chain"),
"rebased branch should be visible as a heddle thread: {names:?}"
);
let log = heddle_with_host_git(&["--json", "log", "feature/chain", "-n", "10"], &work).unwrap();
let log: Value = serde_json::from_str(&log).unwrap();
let intents: Vec<String> = log["states"]
.as_array()
.unwrap()
.iter()
.filter_map(|s| s["intent"].as_str().map(|s| s.to_string()))
.collect();
let step_count = intents.iter().filter(|i| i.starts_with("step ")).count();
assert!(
step_count >= 5,
"rebased chain should contribute ≥5 step states: {intents:?}"
);
}
#[test]
#[ignore = "nightly real-world matrix: multi-remote divergence"]
fn realworld_git_multi_remote_divergent_main_resolves_origin_first() {
let temp = TempDir::new().unwrap();
let origin = temp.path().join("origin.git");
let upstream = temp.path().join("upstream.git");
let work = temp.path().join("work");
let origin_repo = gix::init_bare(&origin).expect("init origin");
let upstream_repo = gix::init_bare(&upstream).expect("init upstream");
let tree_a = git_tree_with_file(&origin_repo, "core.rs", b"fn a() {}\n");
let a = git_commit_with_tree(&origin_repo, Some("refs/heads/main"), tree_a, "A", &[]);
let tree_b = git_tree_with_file(&origin_repo, "core.rs", b"fn a() {}\nfn b() {}\n");
let _b = git_commit_with_tree(&origin_repo, Some("refs/heads/main"), tree_b, "B", &[a]);
git_set_reference(&origin_repo, "HEAD", a);
let tree_a2 = git_tree_with_file(&upstream_repo, "core.rs", b"fn a_prime() {}\n");
let a2 = git_commit_with_tree(&upstream_repo, Some("refs/heads/main"), tree_a2, "A'", &[]);
let tree_c = git_tree_with_file(&upstream_repo, "core.rs", b"fn a_prime() {}\nfn c() {}\n");
let _c = git_commit_with_tree(&upstream_repo, Some("refs/heads/main"), tree_c, "C", &[a2]);
heddle_without_git(
&["clone", origin.to_str().unwrap(), work.to_str().unwrap()],
temp.path(),
)
.unwrap();
let config_path = work.join(".git").join("config");
let mut existing = std::fs::read_to_string(&config_path).unwrap_or_default();
existing.push_str(&format!(
"\n[remote \"upstream\"]\n\turl = {}\n\tfetch = +refs/heads/*:refs/remotes/upstream/*\n",
upstream.display()
));
std::fs::write(&config_path, existing).unwrap();
let listing = heddle_with_host_git(&["remote", "list"], &work).unwrap();
assert!(
listing.contains("origin") && listing.contains("upstream"),
"remote list should expose both remotes: {listing}"
);
}
#[test]
#[ignore = "nightly real-world matrix: annotated tag rename + re-annotate"]
fn realworld_git_annotated_tag_rename_round_trips() {
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 tree_a = git_tree_with_file(&origin_repo, "core.rs", b"fn a() {}\n");
let a = git_commit_with_tree(&origin_repo, Some("refs/heads/main"), tree_a, "A", &[]);
let tree_b = git_tree_with_file(&origin_repo, "core.rs", b"fn a() {}\nfn b() {}\n");
let b = git_commit_with_tree(&origin_repo, Some("refs/heads/main"), tree_b, "B", &[a]);
git_set_reference(&origin_repo, "HEAD", a);
let tag_a = origin_repo
.tag(
"v0.1",
a,
gix::object::Kind::Commit,
None,
"v0.1 release\n",
gix::refs::transaction::PreviousValue::Any,
)
.expect("tag v0.1");
let tag_b = origin_repo
.tag(
"v0.1",
b,
gix::object::Kind::Commit,
None,
"v0.1 retargeted to B\n",
gix::refs::transaction::PreviousValue::Any,
)
.expect("retarget v0.1");
assert_ne!(
tag_a.id(),
tag_b.id(),
"retargeting should mint a new tag oid"
);
heddle_without_git(
&["clone", origin.to_str().unwrap(), work.to_str().unwrap()],
temp.path(),
)
.unwrap();
heddle_without_git(&["bridge", "import"], &work).unwrap();
let mirror = work.join(".heddle").join("git");
let mirror_repo = gix::open(&mirror).expect("open bridge mirror");
let tag_ref = mirror_repo
.find_reference("refs/tags/v0.1")
.expect("v0.1 ref present");
let tag_oid = tag_ref.target().try_id().expect("tag oid").to_owned();
assert_eq!(
tag_oid,
tag_b.id(),
"bridge mirror should track the retargeted tag oid"
);
assert!(
mirror_repo.find_object(a).is_ok(),
"original commit A must remain reachable in the mirror"
);
assert!(
mirror_repo.find_object(b).is_ok(),
"retargeted commit B must remain reachable in the mirror"
);
}
#[test]
#[ignore = "nightly real-world matrix: cherry-pick distinctness"]
fn realworld_git_cherry_pick_assigns_distinct_change_ids() {
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 base_tree = git_tree_with_file(&origin_repo, "core.rs", b"fn base() {}\n");
let base = git_commit_with_tree(
&origin_repo,
Some("refs/heads/main"),
base_tree,
"base",
&[],
);
git_set_reference(&origin_repo, "HEAD", base);
let fix_tree = git_tree_with_file(
&origin_repo,
"core.rs",
b"fn base() {}\nfn fix() { /* the fix */ }\n",
);
let fix_a = git_commit_with_tree(
&origin_repo,
Some("refs/heads/feature/a"),
fix_tree,
"apply fix",
&[base],
);
let intermediate_tree =
git_tree_with_file(&origin_repo, "core.rs", b"fn base() {}\n// preparing\n");
let intermediate = git_commit_with_tree(
&origin_repo,
Some("refs/heads/feature/b"),
intermediate_tree,
"preparing for cherry-pick",
&[base],
);
let fix_b = git_commit_with_tree(
&origin_repo,
Some("refs/heads/feature/b"),
fix_tree,
"apply fix",
&[intermediate],
);
assert_ne!(
fix_a, fix_b,
"cherry-pick onto a different parent must mint a distinct commit oid"
);
heddle_without_git(
&["clone", origin.to_str().unwrap(), work.to_str().unwrap()],
temp.path(),
)
.unwrap();
heddle_without_git(&["bridge", "import"], &work).unwrap();
let log_a: Value = serde_json::from_str(
&heddle_with_host_git(&["--json", "log", "feature/a", "-n", "1"], &work).unwrap(),
)
.unwrap();
let log_b: Value = serde_json::from_str(
&heddle_with_host_git(&["--json", "log", "feature/b", "-n", "1"], &work).unwrap(),
)
.unwrap();
let id_a = log_a["states"][0]["change_id"]
.as_str()
.unwrap()
.to_string();
let id_b = log_b["states"][0]["change_id"]
.as_str()
.unwrap()
.to_string();
assert_ne!(
id_a, id_b,
"cherry-picked commits must mint distinct heddle change ids — got {id_a} on both"
);
}
#[test]
#[ignore = "nightly real-world matrix: gc mapping prune"]
fn realworld_git_gc_prunes_unreachable_mapping_entries() {
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 base_tree = git_tree_with_file(&origin_repo, "core.rs", b"fn base() {}\n");
let base = git_commit_with_tree(
&origin_repo,
Some("refs/heads/main"),
base_tree,
"base",
&[],
);
git_set_reference(&origin_repo, "HEAD", base);
heddle_without_git(
&["clone", origin.to_str().unwrap(), work.to_str().unwrap()],
temp.path(),
)
.unwrap();
heddle_without_git(&["bridge", "import"], &work).unwrap();
let mapping_path = work
.join(".heddle")
.join("git-bridge")
.join("bridge-mapping.json");
let mapping_text = std::fs::read_to_string(&mapping_path).expect("mapping json");
let original_entries = mapping_text.matches("\"change_id\"").count();
let mut value: Value = serde_json::from_str(&mapping_text).unwrap();
let entries = value["entries"].as_array_mut().unwrap();
entries.push(serde_json::json!({
"change_id": "hd-aaaaaaaaaaaaaaaaaaaaaaaaaa",
"git_oid": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
}));
std::fs::write(&mapping_path, serde_json::to_string_pretty(&value).unwrap()).unwrap();
heddle_with_host_git(&["gc"], &work).unwrap();
let post_text = std::fs::read_to_string(&mapping_path).unwrap();
let post_value: Value = serde_json::from_str(&post_text).unwrap();
let post_entries = post_value["entries"].as_array().unwrap();
let post_count = post_entries.len();
assert!(
post_count <= original_entries,
"gc must prune at least the synthetic stale entry: had {} → {}",
original_entries,
post_count
);
assert!(
!post_text.contains("deadbeef"),
"stale mapping entry should be gone after gc: {post_text}"
);
}
#[test]
#[ignore = "nightly real-world matrix: clone + import each vendored fixture"]
fn realworld_fixtures_clone_and_import_round_trip() {
let registry = registry();
for entry in registry.repo {
let (_fix, bare) = extract_fixture(&entry.name);
let work_root = TempDir::new().unwrap();
let work = work_root.path().join("work");
heddle_without_git(
&["clone", bare.to_str().unwrap(), work.to_str().unwrap()],
work_root.path(),
)
.unwrap_or_else(|err| panic!("heddle clone failed for {}: {err}", entry.name));
heddle_without_git(&["bridge", "import"], &work)
.unwrap_or_else(|err| panic!("bridge import failed for {}: {err}", entry.name));
let fsck = heddle_with_host_git(&["fsck", "--bridge", "--json"], &work)
.unwrap_or_else(|err| panic!("fsck --bridge failed for {}: {err}", entry.name));
let parsed: Value = serde_json::from_str(&fsck)
.unwrap_or_else(|_| panic!("fsck output should parse for {}: {fsck}", entry.name));
assert_eq!(
parsed["valid"], true,
"{} should fsck cleanly after clone+import: {fsck}",
entry.name
);
let threads = heddle_with_host_git(&["thread", "list", "--json"], &work)
.unwrap_or_else(|err| panic!("thread list failed for {}: {err}", entry.name));
let threads_json: Value = serde_json::from_str(&threads).unwrap();
let names: Vec<String> = threads_json["threads"]
.as_array()
.expect("threads array")
.iter()
.filter_map(|t| t["name"].as_str().map(|s| s.to_string()))
.collect();
assert!(
!names.is_empty(),
"{} should expose at least the active branch as a thread; saw: {names:?}",
entry.name
);
}
}
#[test]
#[ignore = "nightly real-world matrix: walk every marketing-useful moment"]
fn marketing_moments_walkthrough_against_real_fixture() {
let (_fix, bare) = extract_fixture("ripgrep-shaped");
let work_root = TempDir::new().unwrap();
let work = work_root.path().join("work");
heddle_without_git(
&["clone", bare.to_str().unwrap(), work.to_str().unwrap()],
work_root.path(),
)
.unwrap();
let status_json = heddle_with_host_git(&["--json", "status"], &work).unwrap();
let status: Value = serde_json::from_str(&status_json).unwrap();
assert_eq!(
status["thread"].as_str(),
Some("master"),
"(M1) heddle status should expose current branch as a Heddle thread: {status_json}"
);
assert_eq!(
status["repository_capability"].as_str(),
Some("git-overlay"),
"(M1) heddle status should report git-overlay capability on a cloned-from-git repo: {status_json}"
);
let cloned = gix::open(&work).expect("open cloned working tree");
let head = cloned
.head_commit()
.expect("cloned repo should have a HEAD commit");
git_set_reference(&cloned, "refs/heads/raw-side-branch", head.id().detach());
heddle_without_git(&["bridge", "import"], &work).unwrap();
let threads_json = heddle_with_host_git(&["thread", "list", "--json"], &work).unwrap();
let threads: Value = serde_json::from_str(&threads_json).unwrap();
let names: Vec<String> = threads["threads"]
.as_array()
.unwrap()
.iter()
.filter_map(|t| t["name"].as_str().map(|s| s.to_string()))
.collect();
assert!(
names.iter().any(|n| n == "raw-side-branch"),
"(M10) raw git branch should surface as a heddle thread post-import: {names:?}"
);
for thread in threads["threads"].as_array().unwrap() {
assert!(
thread["name"].as_str().is_some(),
"(M3) every thread row needs a name: {thread}"
);
}
let mut agent_paths = Vec::new();
for (slug, task) in [
("agent/risk-copy", "Tighten risk copy"),
("agent/owner-defaults", "Improve owner fallback behavior"),
(
"agent/status-summary",
"Make release summary more descriptive",
),
] {
let started_json = heddle_with_host_git(
&[
"--json",
"start",
slug,
"--workspace",
"private",
"--task",
task,
"--agent-provider",
"anthropic",
"--agent-model",
"claude-sonnet-4-6",
],
&work,
)
.unwrap_or_else(|err| panic!("(M2) heddle start failed for {slug}: {err}"));
let started: Value = serde_json::from_str(&started_json).unwrap();
let path = started["execution_path"]
.as_str()
.unwrap_or_else(|| panic!("(M2) start should report execution_path for {slug}"))
.to_string();
agent_paths.push((slug.to_string(), std::path::PathBuf::from(path)));
}
for (slug, path) in &agent_paths {
let filename = format!("note-{}.txt", slug.replace('/', "-"));
std::fs::write(path.join(&filename), format!("note from {slug}\n")).unwrap();
let on_disk = std::fs::read_to_string(path.join(&filename)).unwrap();
assert!(
on_disk.contains(slug),
"(M4) overlapping edits should stay isolated; {slug} saw: {on_disk}"
);
}
for (slug, _) in &agent_paths {
let filename = format!("note-{}.txt", slug.replace('/', "-"));
assert!(
!work.join(&filename).exists(),
"(M4) base workspace should not see {slug}'s writes"
);
}
let owner_path = &agent_paths[1].1;
assert!(
!owner_path.join("note-agent-risk-copy.txt").exists(),
"(M4) owner-defaults thread must not see risk-copy's writes"
);
for (slug, path) in &agent_paths {
let cap_json = heddle_with_host_git(
&[
"--json",
"capture",
"--intent",
&format!("draft work for {slug}"),
"--confidence",
"0.85",
],
path,
)
.unwrap_or_else(|err| panic!("(M5) capture failed on {slug}: {err}"));
let cap: Value = serde_json::from_str(&cap_json).unwrap();
assert_eq!(cap["intent"], format!("draft work for {slug}"));
assert!(
cap["confidence"].as_f64().is_some(),
"(M5) capture should echo confidence: {cap_json}"
);
}
let post_capture_threads: Value =
serde_json::from_str(&heddle_with_host_git(&["--json", "thread", "list"], &work).unwrap())
.unwrap();
let ahead_count = post_capture_threads["threads"]
.as_array()
.unwrap()
.iter()
.filter(|t| t["coordination_status"].as_str() == Some("ahead"))
.count();
assert!(
ahead_count >= agent_paths.len(),
"(M3/M5) all three agent threads should report `ahead` after capture: {post_capture_threads}"
);
let _ = heddle_with_host_git(&["ready", "--thread", "agent/risk-copy"], &work)
.unwrap_or_else(|err| panic!("(M6) ready failed for risk-copy: {err}"));
let merge_first = heddle_with_host_git(
&["merge", "agent/risk-copy", "-m", "Merge risk copy thread"],
&work,
);
assert!(
merge_first.is_ok(),
"(M6) first thread merge should succeed: {merge_first:?}"
);
let post_merge: Value =
serde_json::from_str(&heddle_with_host_git(&["--json", "thread", "list"], &work).unwrap())
.unwrap();
let stale_remaining: Vec<&str> = post_merge["threads"]
.as_array()
.unwrap()
.iter()
.filter_map(|t| {
let name = t["name"].as_str()?;
if name == "agent/owner-defaults" || name == "agent/status-summary" {
Some(name)
} else {
None
}
})
.collect();
assert_eq!(
stale_remaining.len(),
2,
"(M6) the two un-merged agent threads should still be visible after merging risk-copy: {post_merge}"
);
let checkpoint_out = heddle_with_host_git(
&["--json", "checkpoint", "-m", "Checkpoint integrated work"],
&work,
)
.unwrap_or_else(|err| panic!("(M11) checkpoint failed: {err}"));
let checkpoint: Value =
serde_json::from_str(&checkpoint_out).expect("(M11) checkpoint output should parse");
assert!(
checkpoint["state"].as_str().is_some()
|| checkpoint["change_id"].as_str().is_some()
|| checkpoint["recorded"].as_bool().unwrap_or(false),
"(M11) checkpoint should report the new state/change_id: {checkpoint_out}"
);
let continue_out = heddle_with_host_git(&["continue"], &work).unwrap_or_else(|err| {
err
});
assert!(
!continue_out.contains("error: unrecognized")
&& !continue_out.contains("error: no such")
&& !continue_out.is_empty(),
"(M7) heddle continue verb must be wired up; got: {continue_out}"
);
let abort_out = heddle_with_host_git(&["abort"], &work).unwrap_or_else(|err| err);
assert!(
!abort_out.contains("error: unrecognized") && !abort_out.contains("error: no such"),
"(M12) heddle abort verb must be wired up; got: {abort_out}"
);
let refresh_out = heddle_with_host_git(&["thread", "refresh", "agent/owner-defaults"], &work);
assert!(
refresh_out.is_ok(),
"(M9) thread refresh on stale non-overlapping thread should succeed: {refresh_out:?}"
);
}