#![cfg(target_family = "unix")]
use std::process::Command;
use std::time::Duration;
use tempfile::tempdir;
fn bin_path() -> &'static str {
env!("CARGO_BIN_EXE_runex")
}
fn bash4_available() -> bool {
let Ok(path) = which::which("bash") else { return false };
let out = Command::new(path)
.args(["--norc", "--noprofile", "-c", "echo $BASH_VERSION"])
.output();
let Ok(out) = out else { return false };
let ver = String::from_utf8_lossy(&out.stdout);
ver.trim()
.split('.')
.next()
.and_then(|s| s.parse::<u32>().ok())
.is_some_and(|major| major >= 4)
}
fn user_runs_init_bash(home: &std::path::Path) -> std::path::PathBuf {
let bin = bin_path();
let cfg_dir = home.join(".config").join("runex");
std::fs::create_dir_all(&cfg_dir).unwrap();
let cfg = cfg_dir.join("config.toml");
std::fs::write(
&cfg,
"version = 1\n\n[keybind.trigger]\ndefault = \"space\"\n\n[[abbr]]\nkey = \"gst\"\nexpand = \"echo EXPANDED\"\n",
)
.unwrap();
let out = Command::new(bin)
.env("HOME", home)
.env("USERPROFILE", home)
.env("XDG_CACHE_HOME", home.join(".cache"))
.env("XDG_CONFIG_HOME", home.join(".config"))
.env("SHELL", "/bin/bash")
.args(["--config", cfg.to_str().unwrap(), "init", "bash", "--yes"])
.output()
.unwrap();
assert!(
out.status.success(),
"`runex init bash --yes` must succeed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
home.join(".bashrc")
}
#[test]
fn user_typed_trigger_key_expands_via_static_cache() {
if !bash4_available() {
eprintln!("skipping: bash 4+ not available");
return;
}
let dir = tempdir().unwrap();
let home = dir.path();
let rcfile = user_runs_init_bash(home);
let cache = home.join(".cache").join("runex").join("integration.bash");
assert!(cache.exists(), "init must have written the cache file at {}", cache.display());
assert!(rcfile.exists(), "init must have written the rcfile at {}", rcfile.display());
let bash = which::which("bash").expect("bash on PATH");
let mut session = expectrl::spawn(format!(
"{bash} --rcfile {rc} -i",
bash = bash.display(),
rc = rcfile.display()
))
.expect("spawn interactive bash");
session.set_expect_timeout(Some(Duration::from_secs(5)));
session
.send_line(&format!("export HOME={}", home.display()))
.ok();
session
.send_line(&format!(
"export XDG_CACHE_HOME={} XDG_CONFIG_HOME={}",
home.join(".cache").display(),
home.join(".config").display()
))
.ok();
session
.send_line("bind 'set enable-bracketed-paste off' 2>/dev/null")
.ok();
session.send_line("PS1='__PTY__ '").ok();
use expectrl::Regex;
session.expect(Regex(r"__PTY__\s*$")).ok();
session.send("gst ").ok();
session.send_line("").ok();
let saw_expanded = session.expect(Regex(r"EXPANDED")).is_ok();
session.send_line("exit").ok();
assert!(
saw_expanded,
"the rcfile written by `runex init bash` must wire up the Space \
binding so that `gst<Space>` expands to `echo EXPANDED` and bash \
runs it; instead the PTY never saw the word EXPANDED in stdout. \
This is the regression that would otherwise hide behind the existing \
`bash_pty_integration.rs`'s legacy-eval bootstrap path."
);
}