use std::path::PathBuf;
use std::process::Command;
fn linthis_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_linthis"))
}
fn run(home: &std::path::Path, args: &[&str]) -> std::process::Output {
Command::new(linthis_bin())
.env("HOME", home)
.env_remove("LINTHIS_HOOK_COLOR")
.args(args)
.output()
.expect("spawn linthis")
}
#[test]
fn add_alias_then_remove_round_trip() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
let out = run(home, &["shell", "add", "alias", "--shell", "bash"]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let bashrc = std::fs::read_to_string(home.join(".bashrc")).unwrap();
assert!(bashrc.contains("# >>> linthis shell init >>>"));
assert!(bashrc.contains(".linthis/shell.bash"));
let source = std::fs::read_to_string(home.join(".linthis/shell.bash")).unwrap();
assert!(source.contains("alias lt='linthis'"));
assert!(source.contains("ltr() { linthis report show \"$@\"; }"));
assert!(
!source.contains("eval \"$(linthis shell completion bash"),
"ac was not requested but appeared in source: {source}"
);
let state = std::fs::read_to_string(home.join(".linthis/shell-state.toml")).unwrap();
assert!(state.contains("[bash]") && state.contains("alias = true"));
let out2 = run(home, &["shell", "add", "alias", "--shell", "bash"]);
assert!(out2.status.success());
let source2 = std::fs::read_to_string(home.join(".linthis/shell.bash")).unwrap();
assert_eq!(source, source2);
let out3 = run(home, &["shell", "add", "ac", "--shell", "bash"]);
assert!(out3.status.success());
let source3 = std::fs::read_to_string(home.join(".linthis/shell.bash")).unwrap();
assert!(source3.contains("eval \"$(linthis shell completion bash"));
assert!(source3.contains("alias lt='linthis'"));
let out4 = run(home, &["shell", "remove", "ac", "--shell", "bash"]);
assert!(out4.status.success());
let source4 = std::fs::read_to_string(home.join(".linthis/shell.bash")).unwrap();
assert!(!source4.contains("eval \"$(linthis shell completion bash"));
assert!(source4.contains("alias lt='linthis'"));
assert!(
home.join(".bashrc").exists(),
"rc still has alias section, marker should remain"
);
let out5 = run(home, &["shell", "remove", "alias", "--shell", "bash"]);
assert!(out5.status.success());
assert!(
!home.join(".linthis/shell.bash").exists(),
"source file should be deleted"
);
let bashrc_final = std::fs::read_to_string(home.join(".bashrc")).unwrap();
assert!(
!bashrc_final.contains("# >>> linthis shell init >>>"),
"marker block should be gone: {bashrc_final}"
);
}
#[test]
fn add_all_targets_all_four_shells() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
let out = run(home, &["shell", "add", "all", "--shell", "all"]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
for ext in ["bash", "zsh", "fish", "ps1"] {
let p = home.join(".linthis").join(format!("shell.{ext}"));
assert!(p.exists(), "missing {p:?}");
}
assert!(home.join(".bashrc").exists());
assert!(home.join(".zshrc").exists());
assert!(home.join(".config/fish/config.fish").exists());
assert!(home
.join("Documents/PowerShell/Microsoft.PowerShell_profile.ps1")
.exists());
}
#[test]
fn unknown_feature_exits_nonzero() {
let dir = tempfile::tempdir().unwrap();
let out = run(
dir.path(),
&["shell", "add", "everything", "--shell", "bash"],
);
assert!(!out.status.success(), "expected nonzero exit");
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("unknown feature"), "stderr: {err}");
}
#[test]
fn unknown_shell_exits_nonzero() {
let dir = tempfile::tempdir().unwrap();
let out = run(dir.path(), &["shell", "add", "ac", "--shell", "elvish"]);
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("unrecognized") || err.contains("elvish"));
}
#[test]
fn status_surfaces_unmanaged_source_line() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
std::fs::write(
home.join(".bashrc"),
"# user content\nsource $HOME/.linthis/shell.bash\nexport FOO=1\n",
)
.unwrap();
let out = run(home, &["shell", "status"]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("unmanaged"),
"status should surface unmanaged condition. Got:\n{stdout}"
);
let zsh_line = stdout
.lines()
.find(|l| l.contains("zsh"))
.expect("zsh row missing");
assert!(
!zsh_line.contains("unmanaged"),
"zsh should not be tagged unmanaged: {zsh_line}"
);
}
#[test]
fn bash_add_writes_bash_profile_shim_when_bash_profile_exists() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
std::fs::write(
home.join(".bash_profile"),
"# existing user content\nexport USER_VAR=1\n",
)
.unwrap();
let out = run(home, &["shell", "add", "ac", "--shell", "bash"]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let bp = std::fs::read_to_string(home.join(".bash_profile")).unwrap();
assert!(
bp.contains("# >>> linthis bash_profile shim >>>"),
"expected shim marker in .bash_profile: {bp}"
);
assert!(
bp.contains("[ -f \"$HOME/.bashrc\" ] && . \"$HOME/.bashrc\""),
"expected source-bashrc shim line: {bp}"
);
assert!(bp.contains("export USER_VAR=1"));
let out2 = run(home, &["shell", "remove", "all", "--shell", "bash"]);
assert!(out2.status.success());
let bp_after = std::fs::read_to_string(home.join(".bash_profile")).unwrap();
assert!(
!bp_after.contains("# >>> linthis bash_profile shim >>>"),
"shim should be gone after remove all: {bp_after}"
);
assert!(bp_after.contains("export USER_VAR=1"));
}
#[test]
fn bash_add_skips_shim_when_bash_profile_missing() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
let out = run(home, &["shell", "add", "ac", "--shell", "bash"]);
assert!(out.status.success());
assert!(
!home.join(".bash_profile").exists(),
".bash_profile should not be auto-created when missing"
);
}
#[test]
fn bash_add_skips_shim_when_bash_profile_already_sources_bashrc() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
std::fs::write(
home.join(".bash_profile"),
"[ -f ~/.bashrc ] && . ~/.bashrc\n",
)
.unwrap();
let out = run(home, &["shell", "add", "ac", "--shell", "bash"]);
assert!(out.status.success());
let bp = std::fs::read_to_string(home.join(".bash_profile")).unwrap();
assert!(
!bp.contains("# >>> linthis bash_profile shim >>>"),
"shim should NOT be added when user already sources bashrc: {bp}"
);
assert!(bp.contains("[ -f ~/.bashrc ] && . ~/.bashrc"));
}