use super::*;
#[test]
fn materialized_thread_full_lifecycle() {
let main = setup_repo("hello.txt", "hello world");
fs::write(main.path().join("README.md"), "# project\n").unwrap();
fs::create_dir_all(main.path().join("src")).unwrap();
fs::write(main.path().join("src/lib.rs"), "fn main() {}\n").unwrap();
heddle(&["capture", "-m", "seed multi-file"], Some(main.path())).unwrap();
let thread_dir = TempDir::new().unwrap();
let thread_path = thread_dir.path();
let started_json = heddle(
&[
"--output",
"json",
"start",
"feature/m-thread",
"--workspace",
"materialized",
"--path",
thread_path.to_str().unwrap(),
],
Some(main.path()),
)
.unwrap();
let started: Value = serde_json::from_str(&started_json).unwrap();
assert_eq!(started["thread"]["thread_mode"], "materialized");
for f in &["hello.txt", "README.md", "src/lib.rs"] {
let p = thread_path.join(f);
assert!(
p.exists(),
"materialise should produce {f} at {}",
p.display()
);
}
let manifest_dir =
repo::thread_manifest::thread_dir(&main.path().join(".heddle"), "feature/m-thread");
let manifest_path = manifest_dir.join("manifest.toml");
assert!(
manifest_path.is_file(),
"manifest must be written at {}",
manifest_path.display()
);
let manifest_v1 = fs::read_to_string(&manifest_path).unwrap();
assert!(
manifest_v1.contains("schema_version"),
"manifest must record schema_version: {manifest_v1}"
);
fs::write(thread_path.join("hello.txt"), "hello edits").unwrap();
fs::write(
thread_path.join("src/lib.rs"),
"fn main() { println!(\"hi\"); }\n",
)
.unwrap();
let capture_json = heddle(
&["--output", "json", "capture", "-m", "agent work"],
Some(thread_path),
)
.unwrap();
let captured: Value = serde_json::from_str(&capture_json).unwrap();
let captured_state = captured["change_id"]
.as_str()
.expect("capture json carries change_id")
.to_string();
assert!(
!captured_state.is_empty(),
"capture should report a new state id"
);
let manifest_v2 = fs::read_to_string(&manifest_path).unwrap();
assert_ne!(
manifest_v1, manifest_v2,
"manifest must be refreshed after capture inside the materialised worktree"
);
let status_json = heddle(&["--output", "json", "status"], Some(main.path())).unwrap();
let status: Value = serde_json::from_str(&status_json).unwrap();
let materialized = status["materialized_threads"]
.as_array()
.expect("materialized_threads array must be present in JSON");
let entry = materialized
.iter()
.find(|m| m["name"] == "feature/m-thread")
.expect("status JSON should list our materialised thread");
assert_eq!(
entry["stale"], false,
"thread should not be stale after a fresh capture"
);
assert!(
entry["file_count"].as_u64().unwrap_or(0) >= 3,
"manifest should still track all 3 files from the seed"
);
heddle(&["thread", "drop", "feature/m-thread"], Some(main.path())).unwrap();
assert!(
!thread_path.exists(),
"thread drop must remove the materialised checkout at {}",
thread_path.display()
);
assert!(
!manifest_dir.exists(),
"thread drop must remove the manifest sidecar dir at {}",
manifest_dir.display()
);
let status_after_json = heddle(&["--output", "json", "status"], Some(main.path())).unwrap();
let status_after: Value = serde_json::from_str(&status_after_json).unwrap();
let materialized_after = status_after
.get("materialized_threads")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
assert!(
materialized_after
.iter()
.all(|m| m["name"] != "feature/m-thread"),
"dropped thread must vanish from status inventory: {materialized_after:?}"
);
}
#[test]
fn short_status_surfaces_stale_materialized_thread_advisory() {
let main = setup_repo("hello.txt", "hello\n");
let thread_dir = TempDir::new().unwrap();
let thread_path = thread_dir.path();
heddle(
&[
"start",
"feature/short-stale",
"--workspace",
"materialized",
"--path",
thread_path.to_str().unwrap(),
],
Some(main.path()),
)
.unwrap();
let manifest_path =
repo::thread_manifest::manifest_path(&main.path().join(".heddle"), "feature/short-stale");
assert!(
manifest_path.is_file(),
"manifest expected at {} after materialized start",
manifest_path.display()
);
let manifest = fs::read_to_string(&manifest_path).unwrap();
let mut doc: toml::Value = toml::from_str(&manifest).unwrap();
let stale_state_id = toml::Value::Array(
(0u8..16)
.map(|b| toml::Value::Integer((b ^ 0xa5) as i64))
.collect(),
);
doc.as_table_mut()
.unwrap()
.insert("state_id".to_string(), stale_state_id);
fs::write(&manifest_path, toml::to_string(&doc).unwrap()).unwrap();
let json_out = heddle(&["--output", "json", "status"], Some(main.path())).unwrap();
let json: Value = serde_json::from_str(&json_out).unwrap();
let entry = json["materialized_threads"]
.as_array()
.and_then(|arr| arr.iter().find(|m| m["name"] == "feature/short-stale"))
.unwrap_or_else(|| {
panic!("json status must list feature/short-stale after manifest rewrite:\n{json_out}")
});
assert_eq!(
entry["stale"], true,
"manifest rewrite should mark thread stale in JSON output too"
);
let short = heddle(&["status", "--short"], Some(main.path())).unwrap();
assert!(
short.contains("materialized thread(s) lag their head"),
"`heddle status --short` must emit the materialised-thread \
staleness advisory when a checkout lags its head; got:\n{short}"
);
assert!(
short.contains("feature/short-stale"),
"advisory must name the stale thread; got:\n{short}"
);
}