use super::*;
fn init_rust_workspace(dir: &std::path::Path) {
std::fs::write(
dir.join("Cargo.toml"),
b"[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.unwrap();
}
#[test]
fn shared_target_writes_cargo_config_pointing_to_shared_dir() {
let temp = TempDir::new().unwrap();
init_rust_workspace(temp.path());
heddle(&["init"], Some(temp.path())).unwrap();
std::fs::write(temp.path().join("main.txt"), "main").unwrap();
heddle(&["capture", "-m", "main"], Some(temp.path())).unwrap();
let thread_path = temp.path().join("probe-a");
heddle(
&[
"start",
"probe-a",
"--path",
thread_path.to_str().unwrap(),
"--shared-target",
],
Some(temp.path()),
)
.expect("start --shared-target should succeed in a Rust workspace");
let cargo_config = thread_path.join(".cargo").join("config.toml");
assert!(
cargo_config.is_file(),
"expected `.cargo/config.toml` at {}",
cargo_config.display()
);
let body = std::fs::read_to_string(&cargo_config).unwrap();
let parsed: toml::Value = toml::from_str(&body).expect("config.toml is valid TOML");
let target_dir = parsed
.get("build")
.and_then(|t| t.get("target-dir"))
.and_then(|v| v.as_str())
.expect("config has [build].target-dir");
let target_path = std::path::PathBuf::from(target_dir);
assert!(
target_path.is_absolute(),
"shared target dir should be absolute, got {}",
target_dir
);
let parts: Vec<_> = target_path
.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect();
let len = parts.len();
assert!(
len >= 3
&& parts[len - 3] == ".heddle"
&& parts[len - 2] == "targets"
&& !parts[len - 1].is_empty(),
"shared target dir should end in `.heddle/targets/<fingerprint>`, got {}",
target_dir
);
assert!(
target_path.is_dir(),
"shared target dir should exist on disk, got {}",
target_dir
);
}
#[test]
fn shared_target_uses_same_dir_for_two_threads_in_one_workspace() {
let temp = TempDir::new().unwrap();
init_rust_workspace(temp.path());
heddle(&["init"], Some(temp.path())).unwrap();
std::fs::write(temp.path().join("main.txt"), "main").unwrap();
heddle(&["capture", "-m", "main"], Some(temp.path())).unwrap();
let path_a = temp.path().join("probe-a");
let path_b = temp.path().join("probe-b");
heddle(
&[
"start",
"probe-a",
"--path",
path_a.to_str().unwrap(),
"--shared-target",
],
Some(temp.path()),
)
.unwrap();
heddle(
&[
"start",
"probe-b",
"--path",
path_b.to_str().unwrap(),
"--shared-target",
],
Some(temp.path()),
)
.unwrap();
let cfg_a = std::fs::read_to_string(path_a.join(".cargo").join("config.toml")).unwrap();
let cfg_b = std::fs::read_to_string(path_b.join(".cargo").join("config.toml")).unwrap();
let parse = |body: &str| -> String {
let parsed: toml::Value = toml::from_str(body).unwrap();
parsed["build"]["target-dir"].as_str().unwrap().to_string()
};
assert_eq!(
parse(&cfg_a),
parse(&cfg_b),
"both threads in one workspace should share the same target dir",
);
}
#[test]
fn no_flag_means_no_cargo_config() {
let temp = TempDir::new().unwrap();
init_rust_workspace(temp.path());
heddle(&["init"], Some(temp.path())).unwrap();
std::fs::write(temp.path().join("main.txt"), "main").unwrap();
heddle(&["capture", "-m", "main"], Some(temp.path())).unwrap();
let thread_path = temp.path().join("plain");
heddle(
&["start", "plain", "--path", thread_path.to_str().unwrap()],
Some(temp.path()),
)
.unwrap();
assert!(
!thread_path.join(".cargo").join("config.toml").exists(),
"without --shared-target, no .cargo/config.toml should be written",
);
}
#[test]
fn advisory_fires_on_second_thread_without_shared_target() {
let temp = TempDir::new().unwrap();
init_rust_workspace(temp.path());
heddle(&["init"], Some(temp.path())).unwrap();
std::fs::write(temp.path().join("main.txt"), "main").unwrap();
heddle(&["capture", "-m", "main"], Some(temp.path())).unwrap();
let path_a = temp.path().join("first");
let out_a = heddle_output(
&["start", "first", "--path", path_a.to_str().unwrap()],
Some(temp.path()),
)
.unwrap();
assert!(out_a.status.success());
let stderr_a = std::str::from_utf8(&out_a.stderr).unwrap_or("");
assert!(
!stderr_a.contains("--shared-target"),
"first thread should not advise; got stderr: {stderr_a}"
);
let path_b = temp.path().join("second");
let out_b = heddle_output(
&["start", "second", "--path", path_b.to_str().unwrap()],
Some(temp.path()),
)
.unwrap();
assert!(out_b.status.success());
let stderr_b = std::str::from_utf8(&out_b.stderr).unwrap_or("");
assert!(
stderr_b.contains("--shared-target") && stderr_b.contains("second"),
"second thread should advise the flag in stderr; got stderr: {stderr_b}"
);
}
#[test]
fn no_advisory_when_starting_with_shared_target() {
let temp = TempDir::new().unwrap();
init_rust_workspace(temp.path());
heddle(&["init"], Some(temp.path())).unwrap();
std::fs::write(temp.path().join("main.txt"), "main").unwrap();
heddle(&["capture", "-m", "main"], Some(temp.path())).unwrap();
let path_a = temp.path().join("warm-a");
heddle(
&["start", "warm-a", "--path", path_a.to_str().unwrap()],
Some(temp.path()),
)
.unwrap();
let path_b = temp.path().join("warm-b");
let out = heddle_output(
&[
"start",
"warm-b",
"--path",
path_b.to_str().unwrap(),
"--shared-target",
],
Some(temp.path()),
)
.unwrap();
assert!(out.status.success());
let stderr = std::str::from_utf8(&out.stderr).unwrap_or("");
assert!(
!stderr.contains("consider `heddle start --shared-target"),
"passing --shared-target should suppress the nudge; got stderr: {stderr}"
);
}
#[test]
fn no_advisory_in_non_rust_workspace() {
let temp = TempDir::new().unwrap();
heddle(&["init"], Some(temp.path())).unwrap();
std::fs::write(temp.path().join("main.txt"), "main").unwrap();
heddle(&["capture", "-m", "main"], Some(temp.path())).unwrap();
let path_a = temp.path().join("non-rust-a");
heddle(
&["start", "non-rust-a", "--path", path_a.to_str().unwrap()],
Some(temp.path()),
)
.unwrap();
let path_b = temp.path().join("non-rust-b");
let out = heddle_output(
&["start", "non-rust-b", "--path", path_b.to_str().unwrap()],
Some(temp.path()),
)
.unwrap();
assert!(out.status.success());
let stderr = std::str::from_utf8(&out.stderr).unwrap_or("");
assert!(
!stderr.contains("--shared-target"),
"non-Rust workspaces should never see the advisory; got stderr: {stderr}"
);
}
#[test]
fn shared_target_is_noop_in_non_rust_repo() {
let temp = TempDir::new().unwrap();
heddle(&["init"], Some(temp.path())).unwrap();
std::fs::write(temp.path().join("main.txt"), "main").unwrap();
heddle(&["capture", "-m", "main"], Some(temp.path())).unwrap();
let thread_path = temp.path().join("non-rust");
heddle(
&[
"start",
"non-rust",
"--path",
thread_path.to_str().unwrap(),
"--shared-target",
],
Some(temp.path()),
)
.expect("start --shared-target must not error in a non-Rust repo");
assert!(
!thread_path.join(".cargo").join("config.toml").exists(),
"no .cargo/config.toml should be written when there is no Cargo.toml at root"
);
let json = heddle(
&["--output", "json", "thread", "show", "non-rust"],
Some(temp.path()),
)
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let surfaced = parsed
.get("shared_target_dir")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty());
assert!(
surfaced.is_none(),
"non-Rust repo should not advertise a shared target dir: {json}"
);
}
#[test]
fn shared_target_dir_unset_when_user_config_preserved() {
let temp = TempDir::new().unwrap();
init_rust_workspace(temp.path());
heddle(&["init"], Some(temp.path())).unwrap();
let user_config = "[net]\noffline = true\n";
let cargo_dir = temp.path().join(".cargo");
std::fs::create_dir_all(&cargo_dir).unwrap();
std::fs::write(cargo_dir.join("config.toml"), user_config).unwrap();
std::fs::write(temp.path().join("main.txt"), "main").unwrap();
heddle(&["capture", "-m", "main"], Some(temp.path())).unwrap();
let thread_path = temp.path().join("preconfigured");
heddle(
&[
"start",
"preconfigured",
"--path",
thread_path.to_str().unwrap(),
"--shared-target",
],
Some(temp.path()),
)
.expect("start --shared-target should succeed when materialized config exists");
let after = std::fs::read_to_string(thread_path.join(".cargo").join("config.toml")).unwrap();
assert_eq!(
after, user_config,
"user-managed cargo config must be preserved"
);
let json = heddle(
&["--output", "json", "thread", "show", "preconfigured"],
Some(temp.path()),
)
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let surfaced = parsed
.get("shared_target_dir")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty());
assert!(
surfaced.is_none(),
"shared_target_dir must be absent when write_cargo_config was a no-op: {json}"
);
}
#[test]
fn shared_target_dir_surfaces_in_thread_show_json() {
let temp = TempDir::new().unwrap();
init_rust_workspace(temp.path());
heddle(&["init"], Some(temp.path())).unwrap();
std::fs::write(temp.path().join("main.txt"), "main").unwrap();
heddle(&["capture", "-m", "main"], Some(temp.path())).unwrap();
let thread_path = temp.path().join("inspect");
heddle(
&[
"start",
"inspect",
"--path",
thread_path.to_str().unwrap(),
"--shared-target",
],
Some(temp.path()),
)
.unwrap();
let json = heddle(
&["--output", "json", "thread", "show", "inspect"],
Some(temp.path()),
)
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let surfaced = parsed.get("shared_target_dir").and_then(|v| v.as_str());
assert!(
surfaced.is_some(),
"thread show JSON should expose `shared_target_dir`: {json}"
);
let dir = surfaced.unwrap();
assert!(
dir.contains(".heddle"),
"shared_target_dir should sit under .heddle: got {dir}"
);
}