Files
yvgude--lean-ctx/rust/tests/dropin_install_tests.rs
wehub-resource-sync 26382a7ac6
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

421 lines
15 KiB
Rust

#![cfg(unix)]
//! End-to-end coverage for `shell_hook::install_all_with_style`.
//!
//! The unit tests inside `shell_hook.rs` exercise each per-shell install
//! function with an explicit `home: &Path` argument, which keeps them race-
//! free. The tests in this file cover the top-level `install_all_with_style`
//! entry point, which resolves `$HOME` via `dirs::home_dir()`. Because that
//! reads the live env, these tests must run single-threaded — CI already
//! invokes `cargo test --all-features -- --test-threads=1`, and a
//! repo-local mutex below covers the case where someone runs tests
//! without that flag.
//!
//! The intent is to validate the cross-file behaviour:
//! - All four touchpoints (.zshenv, .bashenv, .zshrc, .bashrc) end up in
//! the right style for a given home layout.
//! - Mixed layouts (e.g. dropin for .zshenv but inline for .bashrc) work.
//! - Uninstall removes everything regardless of which style was used.
use std::path::Path;
use std::sync::Mutex;
use lean_ctx::shell_hook::{Style, install_all, install_all_with_style, uninstall_all};
/// Serialises tests in this file so concurrent `$HOME` mutation doesn't
/// race. Cargo runs each integration test binary's tests in parallel by
/// default; this guard plus CI's `--test-threads=1` keeps us correct in
/// both modes.
static HOME_LOCK: Mutex<()> = Mutex::new(());
fn with_home<F: FnOnce(&Path)>(f: F) {
let _guard = HOME_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let tmp = tempfile::tempdir().expect("tempdir");
let prev = std::env::var_os("HOME");
let prev_force = std::env::var_os("LEAN_CTX_SHELL_HOOK_FORCE");
// SAFETY: serialised via HOME_LOCK.
unsafe { std::env::set_var("HOME", tmp.path()) };
// These tests validate the cross-file *install logic*, not host shell
// detection (which has its own unit test). CI runners may lack zsh, so force
// both shells "available" to keep the assertions host-independent.
unsafe { std::env::set_var("LEAN_CTX_SHELL_HOOK_FORCE", "all") };
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(tmp.path())));
match prev {
Some(v) => unsafe { std::env::set_var("HOME", v) },
None => unsafe { std::env::remove_var("HOME") },
}
match prev_force {
Some(v) => unsafe { std::env::set_var("LEAN_CTX_SHELL_HOOK_FORCE", v) },
None => unsafe { std::env::remove_var("LEAN_CTX_SHELL_HOOK_FORCE") },
}
if let Err(p) = result {
std::panic::resume_unwind(p);
}
}
const MARKER_START: &str = "# >>> lean-ctx shell hook >>>";
const MARKER_END: &str = "# <<< lean-ctx shell hook <<<";
const ALIAS_START: &str = "# >>> lean-ctx agent aliases >>>";
const ALIAS_END: &str = "# <<< lean-ctx agent aliases <<<";
fn touch_rc(home: &Path, name: &str) {
std::fs::write(home.join(name), "# placeholder rc\n").unwrap();
}
fn enable_dropin(home: &Path, rc: &str, dir: &str) {
std::fs::create_dir_all(home.join(dir)).unwrap();
std::fs::write(
home.join(rc),
format!("for f in $HOME/{dir}/*.zsh; do source $f; done\n"),
)
.unwrap();
}
// ---------------------------------------------------------------------------
// install_all entry point (default = Auto)
// ---------------------------------------------------------------------------
#[test]
fn install_all_default_is_auto_inline_on_plain_layout() {
with_home(|home| {
touch_rc(home, ".zshrc");
touch_rc(home, ".bashrc");
install_all(true);
let zshenv = std::fs::read_to_string(home.join(".zshenv")).unwrap();
assert!(zshenv.contains(MARKER_START));
let zshrc = std::fs::read_to_string(home.join(".zshrc")).unwrap();
assert!(zshrc.contains(ALIAS_START));
});
}
#[test]
fn install_all_auto_writes_dropin_for_zshenv_when_loop_present() {
with_home(|home| {
enable_dropin(home, ".zshenv", ".zshenv.d");
touch_rc(home, ".zshrc");
install_all_with_style(true, Style::Auto);
let dropin = home.join(".zshenv.d").join("00-lean-ctx.zsh");
assert!(dropin.exists(), "expected drop-in for .zshenv");
let zshenv = std::fs::read_to_string(home.join(".zshenv")).unwrap();
assert!(
!zshenv.contains(MARKER_START),
"drop-in install must not also leave the inline fenced block"
);
});
}
// ---------------------------------------------------------------------------
// Mixed layout: dropin for env, inline for rc
// ---------------------------------------------------------------------------
#[test]
fn auto_resolves_each_slot_independently() {
with_home(|home| {
// zsh env uses .d/ drop-ins (chezmoi style)…
enable_dropin(home, ".zshenv", ".zshenv.d");
// …but zshrc does NOT — user hand-edits it.
std::fs::write(home.join(".zshrc"), "# my plain zshrc\n").unwrap();
install_all_with_style(true, Style::Auto);
assert!(
home.join(".zshenv.d").join("00-lean-ctx.zsh").exists(),
".zshenv hook should be a drop-in"
);
let zshrc = std::fs::read_to_string(home.join(".zshrc")).unwrap();
assert!(
zshrc.contains(ALIAS_START),
".zshrc aliases should be inline"
);
assert!(
!home.join(".zshrc.d").exists(),
"no .zshrc.d should be created when not pre-configured"
);
});
}
// ---------------------------------------------------------------------------
// Migration coverage
// ---------------------------------------------------------------------------
#[test]
fn re_running_after_chezmoi_adoption_migrates_to_dropin() {
with_home(|home| {
// Phase 1: user installs lean-ctx the old way — inline blocks
// everywhere.
touch_rc(home, ".zshrc");
touch_rc(home, ".bashrc");
install_all_with_style(true, Style::Inline);
let inline_zshenv = std::fs::read_to_string(home.join(".zshenv")).unwrap();
assert!(inline_zshenv.contains(MARKER_START));
// Phase 2: user adopts a dotfiles tool that introduces .zshenv.d/
// and changes .zshenv to source it.
std::fs::create_dir_all(home.join(".zshenv.d")).unwrap();
let migrated_zshenv = format!(
"{}\n\nfor f in $HOME/.zshenv.d/*.zsh; do source $f; done\n",
inline_zshenv.trim_end()
);
std::fs::write(home.join(".zshenv"), migrated_zshenv).unwrap();
// Phase 3: lean-ctx update / re-run picks up the new layout.
install_all_with_style(true, Style::Auto);
// The fenced block must be gone from .zshenv …
let final_zshenv = std::fs::read_to_string(home.join(".zshenv")).unwrap();
assert!(
!final_zshenv.contains(MARKER_START),
"migration should strip the legacy fenced block from .zshenv"
);
// …and the drop-in file should now hold the hook.
let dropin_body =
std::fs::read_to_string(home.join(".zshenv.d").join("00-lean-ctx.zsh")).unwrap();
assert!(dropin_body.contains("ZSH_EXECUTION_STRING"));
});
}
#[test]
fn rolling_back_to_inline_removes_dropin_file() {
with_home(|home| {
// Start in drop-in mode.
enable_dropin(home, ".zshenv", ".zshenv.d");
install_all_with_style(true, Style::Auto);
assert!(home.join(".zshenv.d").join("00-lean-ctx.zsh").exists());
// User decides to force inline.
install_all_with_style(true, Style::Inline);
assert!(
!home.join(".zshenv.d").join("00-lean-ctx.zsh").exists(),
"switching back to Inline must remove the drop-in"
);
let zshenv = std::fs::read_to_string(home.join(".zshenv")).unwrap();
assert!(zshenv.contains(MARKER_START));
});
}
// ---------------------------------------------------------------------------
// Uninstall removes everything regardless of style
// ---------------------------------------------------------------------------
#[test]
fn uninstall_removes_inline_artifacts() {
with_home(|home| {
touch_rc(home, ".zshrc");
touch_rc(home, ".bashrc");
install_all_with_style(true, Style::Inline);
uninstall_all(true);
let zshenv = std::fs::read_to_string(home.join(".zshenv")).unwrap_or_default();
assert!(!zshenv.contains(MARKER_START));
let zshrc = std::fs::read_to_string(home.join(".zshrc")).unwrap();
assert!(!zshrc.contains(ALIAS_START));
});
}
#[test]
fn uninstall_removes_dropin_artifacts() {
with_home(|home| {
enable_dropin(home, ".zshenv", ".zshenv.d");
enable_dropin(home, ".zshrc", ".zshrc.d");
install_all_with_style(true, Style::Auto);
assert!(home.join(".zshenv.d").join("00-lean-ctx.zsh").exists());
assert!(home.join(".zshrc.d").join("00-lean-ctx.zsh").exists());
uninstall_all(true);
assert!(!home.join(".zshenv.d").join("00-lean-ctx.zsh").exists());
assert!(!home.join(".zshrc.d").join("00-lean-ctx.zsh").exists());
});
}
#[test]
fn uninstall_removes_both_styles_if_both_present() {
with_home(|home| {
// Pathological state: somehow both inline and drop-in are present.
// Uninstall should clean both.
enable_dropin(home, ".zshenv", ".zshenv.d");
// First install: drop-in.
install_all_with_style(true, Style::Auto);
// Then forcibly add an inline block too (simulating a corrupt
// mid-migration state).
let mut zshenv = std::fs::read_to_string(home.join(".zshenv")).unwrap();
zshenv
.push_str("\n# >>> lean-ctx shell hook >>>\n# stray\n# <<< lean-ctx shell hook <<<\n");
std::fs::write(home.join(".zshenv"), zshenv).unwrap();
uninstall_all(true);
assert!(!home.join(".zshenv.d").join("00-lean-ctx.zsh").exists());
let zshenv = std::fs::read_to_string(home.join(".zshenv")).unwrap();
assert!(!zshenv.contains(MARKER_START));
});
}
#[test]
fn uninstall_is_noop_when_nothing_installed() {
with_home(|home| {
// Empty home — no files. Should not panic, should not create anything.
uninstall_all(true);
assert!(std::fs::read_dir(home).unwrap().next().is_none());
});
}
// ---------------------------------------------------------------------------
// Hand-edit preservation across migration
// ---------------------------------------------------------------------------
/// Returns sibling files of `path` matching
/// `<basename>.lean-ctx-<ts>.bak`.
fn migration_backups_for(path: &std::path::Path) -> Vec<std::path::PathBuf> {
let Some(parent) = path.parent() else {
return Vec::new();
};
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
return Vec::new();
};
let prefix = format!("{name}.lean-ctx-");
let mut out: Vec<_> = std::fs::read_dir(parent)
.into_iter()
.flatten()
.flatten()
.map(|e| e.path())
.filter(|p| {
p.file_name().and_then(|n| n.to_str()).is_some_and(|n| {
n.starts_with(&prefix)
&& std::path::Path::new(n)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("bak"))
})
})
.collect();
out.sort();
out
}
#[test]
fn migration_through_install_all_writes_backup_for_each_migrated_slot() {
with_home(|home| {
// Existing inline install: .zshenv has the fenced hook, .zshrc has
// the fenced aliases. The user has slipped a custom line into the
// .zshenv hook block.
let zshenv_pre = format!(
"# top of .zshenv\n\n\
{MARKER_START}\n\
# CUSTOM: capture pid before lean-ctx execs\n\
echo \"$$\" > /tmp/last-shell-pid\n\
{MARKER_END}\n",
);
std::fs::write(home.join(".zshenv"), &zshenv_pre).unwrap();
std::fs::write(
home.join(".zshrc"),
format!("# top\n{ALIAS_START}\nalias k=kubectl\n{ALIAS_END}\n"),
)
.unwrap();
// User then adopts the .d/ convention for .zshenv.
std::fs::create_dir_all(home.join(".zshenv.d")).unwrap();
let zshenv_with_loop = format!(
"{}\nfor f in $HOME/.zshenv.d/*.zsh; do source $f; done\n",
zshenv_pre.trim_end()
);
std::fs::write(home.join(".zshenv"), &zshenv_with_loop).unwrap();
install_all_with_style(true, Style::Auto);
// .zshenv migrated -> exactly one timestamped backup.
let zshenv_baks = migration_backups_for(&home.join(".zshenv"));
assert_eq!(zshenv_baks.len(), 1, "expected one .zshenv backup");
let bak_body = std::fs::read_to_string(&zshenv_baks[0]).unwrap();
assert!(bak_body.contains("CUSTOM: capture pid"));
assert!(bak_body.contains("last-shell-pid"));
// .zshrc didn't migrate (no .zshrc.d source loop) -> no backup noise.
assert!(
migration_backups_for(&home.join(".zshrc")).is_empty(),
"no backup expected for slot that didn't migrate"
);
});
}
#[test]
fn no_backups_on_clean_install_through_install_all() {
with_home(|home| {
// Fresh user, nothing installed. install_all should not produce
// any .bak files anywhere.
std::fs::create_dir_all(home.join(".zshenv.d")).unwrap();
std::fs::write(
home.join(".zshenv"),
"for f in $HOME/.zshenv.d/*.zsh; do source $f; done\n",
)
.unwrap();
touch_rc(home, ".zshrc");
install_all_with_style(true, Style::Auto);
let mut baks: Vec<_> = walkdir(home)
.filter(|p| {
let s = p.to_string_lossy();
s.contains(".lean-ctx-")
&& std::path::Path::new(&*s)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("bak"))
})
.collect();
baks.sort();
assert!(
baks.is_empty(),
"clean install must not create any .bak files; found: {baks:?}"
);
});
}
fn walkdir(root: &Path) -> impl Iterator<Item = std::path::PathBuf> + use<> {
let mut stack = vec![root.to_path_buf()];
std::iter::from_fn(move || {
while let Some(dir) = stack.pop() {
if let Ok(rd) = std::fs::read_dir(&dir) {
for entry in rd.flatten() {
let p = entry.path();
if p.is_dir() {
stack.push(p);
} else {
return Some(p);
}
}
}
}
None
})
}
#[test]
fn double_install_then_uninstall_leaves_no_trace() {
with_home(|home| {
enable_dropin(home, ".zshenv", ".zshenv.d");
touch_rc(home, ".zshrc");
touch_rc(home, ".bashrc");
install_all_with_style(true, Style::Auto);
install_all_with_style(true, Style::Auto);
uninstall_all(true);
// .zshenv still exists (we only manage our own block); but no
// lean-ctx artifacts should remain.
let zshenv = std::fs::read_to_string(home.join(".zshenv")).unwrap();
assert!(!zshenv.contains(MARKER_START));
assert!(!home.join(".zshenv.d").join("00-lean-ctx.zsh").exists());
let zshrc = std::fs::read_to_string(home.join(".zshrc")).unwrap();
assert!(!zshrc.contains(ALIAS_START));
});
}