use std::fs;
use std::path::Path;
use std::process::{Command, Output};
use tempfile::TempDir;
fn lds_bin() -> String {
std::env::var("CARGO_BIN_EXE_lds")
.unwrap_or_else(|_| format!("{}/../../target/debug/lds", env!("CARGO_MANIFEST_DIR")))
}
fn run_lds(home_dir: &Path, args: &[&str]) -> Output {
Command::new(lds_bin())
.env("HOME", home_dir)
.args(args)
.output()
.expect("failed to spawn lds binary")
}
fn read_config(home_dir: &Path) -> String {
let path = home_dir.join(".config/lds/config.toml");
fs::read_to_string(path).unwrap_or_default()
}
#[test]
fn test_add_creates_config_with_absolute_path() {
let tmp = TempDir::new().expect("TempDir::new");
let home = tmp.path();
let recipe_dir = tmp.path().join("my-recipes");
let recipe_dir_str = recipe_dir.to_string_lossy().to_string();
let out = run_lds(home, &["recipe-dir", "add", &recipe_dir_str]);
assert!(
out.status.success(),
"add should succeed; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let toml = read_config(home);
assert!(
toml.contains(recipe_dir.to_string_lossy().as_ref()),
"config.toml should contain the absolute path; got:\n{toml}"
);
assert!(
!toml.contains('~'),
"tilde literal must not be stored in config.toml; got:\n{toml}"
);
}
#[test]
fn test_add_expands_tilde_to_absolute() {
let tmp = TempDir::new().expect("TempDir::new");
let home = tmp.path();
let out = run_lds(home, &["recipe-dir", "add", "~/cli-test-recipes"]);
assert!(
out.status.success(),
"add with tilde should succeed; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let toml = read_config(home);
let expected_prefix = home.to_string_lossy().to_string();
assert!(
toml.contains(&expected_prefix),
"stored path should start with HOME ({expected_prefix}); got:\n{toml}"
);
assert!(
!toml.contains('~'),
"tilde literal must not be stored in config.toml (crux 2); got:\n{toml}"
);
}
#[test]
fn test_add_preserves_comments_and_other_sections() {
let tmp = TempDir::new().expect("TempDir::new");
let home = tmp.path();
let config_dir = home.join(".config/lds");
fs::create_dir_all(&config_dir).expect("create config dir");
let config_path = config_dir.join("config.toml");
fs::write(
&config_path,
"# My custom lds configuration\n\
[paths]\n\
global_justfile = \"/opt/lds/justfile\"\n",
)
.expect("write seed config");
let recipe_dir = tmp.path().join("new-recipes");
let recipe_dir_str = recipe_dir.to_string_lossy().to_string();
let out = run_lds(home, &["recipe-dir", "add", &recipe_dir_str]);
assert!(
out.status.success(),
"add should succeed; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let toml = read_config(home);
assert!(
toml.contains("# My custom lds configuration"),
"comment must be preserved; got:\n{toml}"
);
assert!(
toml.contains("[paths]"),
"[paths] section must be preserved; got:\n{toml}"
);
assert!(
toml.contains("global_justfile"),
"global_justfile entry must be preserved; got:\n{toml}"
);
assert!(
toml.contains(recipe_dir.to_string_lossy().as_ref()),
"new recipe dir must be added; got:\n{toml}"
);
}
#[test]
fn test_list_prints_paths_in_order() {
let tmp = TempDir::new().expect("TempDir::new");
let home = tmp.path();
let dir_a = tmp.path().join("alpha");
let dir_b = tmp.path().join("beta");
run_lds(home, &["recipe-dir", "add", &dir_a.to_string_lossy()]);
run_lds(home, &["recipe-dir", "add", &dir_b.to_string_lossy()]);
let out = run_lds(home, &["recipe-dir", "list"]);
assert!(
out.status.success(),
"list should succeed; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 2, "expected 2 lines; got:\n{stdout}");
assert!(
lines[0].contains(dir_a.to_string_lossy().as_ref()),
"first line should be dir_a; got:\n{stdout}"
);
assert!(
lines[1].contains(dir_b.to_string_lossy().as_ref()),
"second line should be dir_b; got:\n{stdout}"
);
}
#[test]
fn test_remove_deletes_entry() {
let tmp = TempDir::new().expect("TempDir::new");
let home = tmp.path();
let dir_a = tmp.path().join("alpha");
let dir_b = tmp.path().join("beta");
run_lds(home, &["recipe-dir", "add", &dir_a.to_string_lossy()]);
run_lds(home, &["recipe-dir", "add", &dir_b.to_string_lossy()]);
let out = run_lds(home, &["recipe-dir", "remove", &dir_a.to_string_lossy()]);
assert!(
out.status.success(),
"remove should succeed; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let list_out = run_lds(home, &["recipe-dir", "list"]);
let stdout = String::from_utf8_lossy(&list_out.stdout);
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(
lines.len(),
1,
"only one entry should remain; got:\n{stdout}"
);
assert!(
lines[0].contains(dir_b.to_string_lossy().as_ref()),
"remaining entry should be dir_b; got:\n{stdout}"
);
}
#[test]
fn test_remove_not_found_exits_one() {
let tmp = TempDir::new().expect("TempDir::new");
let home = tmp.path();
let out = run_lds(home, &["recipe-dir", "remove", "/no/such/directory"]);
assert!(
!out.status.success(),
"remove of non-existent path should exit non-zero"
);
assert_eq!(out.status.code(), Some(1), "exit code should be 1");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.is_empty(),
"stderr should contain an error message; got nothing"
);
}
#[test]
fn test_add_duplicate_is_noop() {
let tmp = TempDir::new().expect("TempDir::new");
let home = tmp.path();
let recipe_dir = tmp.path().join("my-recipes");
let recipe_dir_str = recipe_dir.to_string_lossy().to_string();
let first = run_lds(home, &["recipe-dir", "add", &recipe_dir_str]);
assert!(first.status.success(), "first add should succeed");
let second = run_lds(home, &["recipe-dir", "add", &recipe_dir_str]);
assert!(
second.status.success(),
"second add (duplicate) should exit 0; stderr: {}",
String::from_utf8_lossy(&second.stderr)
);
let stderr = String::from_utf8_lossy(&second.stderr);
assert!(
stderr.contains("warn") || stderr.contains("already"),
"second add should emit a warning; stderr: {stderr}"
);
let list_out = run_lds(home, &["recipe-dir", "list"]);
let stdout = String::from_utf8_lossy(&list_out.stdout);
let count = stdout.lines().count();
assert_eq!(
count, 1,
"should have exactly 1 entry after duplicate add; got:\n{stdout}"
);
}