use gwm::aliases::{self, BUILT_IN_ALIASES};
use gwm::config::{resolve_gwm_config_file, Config, CONFIG_FILE};
use gwm::error::GwmError;
use tempfile::TempDir;
#[test]
fn user_alias_file_resolves_under_dotconfig() {
let home = TempDir::new().unwrap();
let platform = TempDir::new().unwrap(); let dir = home.path().join(".config").join("gwm");
std::fs::create_dir_all(&dir).unwrap();
let aliases_file = dir.join("aliases.toml");
std::fs::write(&aliases_file, "").unwrap();
let resolved = resolve_gwm_config_file(
"aliases.toml",
None, Some(home.path()),
Some(platform.path()),
|p| p.exists(),
);
assert_eq!(
resolved,
Some(aliases_file),
"user aliases at the documented ~/.config path must be honoured"
);
}
#[test]
fn config_aliases_default_is_empty() {
let cfg = Config::default();
assert!(cfg.aliases.is_empty());
}
#[test]
fn config_aliases_round_trip_through_toml() {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join(CONFIG_FILE),
r#"
[aliases]
wip = "create feat 0 wip"
ll = "list --format names"
"#,
)
.unwrap();
let cfg = Config::load_layered(dir.path(), None).unwrap();
assert_eq!(cfg.aliases.len(), 2);
assert_eq!(cfg.aliases.get("wip").map(String::as_str), Some("create feat 0 wip"));
assert_eq!(cfg.aliases.get("ll").map(String::as_str), Some("list --format names"));
}
#[test]
fn config_aliases_rejects_shadow_of_built_in_subcommand() {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join(CONFIG_FILE),
r#"
[aliases]
list = "create feat 0 wip"
"#,
)
.unwrap();
let err = Config::load_layered(dir.path(), None).unwrap_err();
match err {
GwmError::Config(msg) => {
assert!(msg.contains("list"), "error must name the offending alias: {msg}");
assert!(msg.contains("built-in"), "error must mention built-in shadowing: {msg}");
}
other => panic!("expected GwmError::Config, got {other:?}"),
}
}
#[test]
fn config_aliases_rejects_shadow_of_visible_built_in_alias() {
for shadow in ["s", "cd"] {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join(CONFIG_FILE),
format!(
r#"
[aliases]
{shadow} = "create feat 0 wip"
"#
),
)
.unwrap();
let err = Config::load_layered(dir.path(), None).unwrap_err();
match err {
GwmError::Config(msg) => {
assert!(msg.contains(shadow), "error must name '{shadow}': {msg}");
}
other => panic!("expected GwmError::Config for '{shadow}', got {other:?}"),
}
}
}
#[test]
fn config_aliases_rejects_shell_pipeline_in_value() {
for bad_value in [
"create feat 0 wip && lazygit",
"path | pbcopy",
"list ; remove x",
"echo `whoami`",
] {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join(CONFIG_FILE),
format!(
r#"
[aliases]
copy = "{bad_value}"
"#
),
)
.unwrap();
let err = Config::load_layered(dir.path(), None).unwrap_err();
match err {
GwmError::Config(msg) => {
assert!(
msg.contains("copy"),
"error must name the offending alias 'copy': {msg}"
);
}
other => panic!("expected GwmError::Config for {bad_value:?}, got {other:?}"),
}
}
}
#[test]
fn config_aliases_rejects_empty_value() {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join(CONFIG_FILE),
r#"
[aliases]
wip = ""
"#,
)
.unwrap();
let err = Config::load_layered(dir.path(), None).unwrap_err();
match err {
GwmError::Config(msg) => assert!(msg.contains("wip"), "{msg}"),
other => panic!("expected GwmError::Config, got {other:?}"),
}
}
#[test]
fn resolved_aliases_contains_built_in_set() {
let dir = TempDir::new().unwrap();
let resolved = aliases::load_layered(Some(dir.path()), None, None).unwrap();
assert!(!resolved.built_in.is_empty());
let s_entry = resolved
.built_in
.iter()
.find(|e| e.name == "s")
.expect("built-in 's' alias must be present");
assert_eq!(s_entry.expansion, "switch");
}
#[test]
fn resolved_aliases_repo_overrides_user_for_same_name() {
let repo_dir = TempDir::new().unwrap();
std::fs::write(
repo_dir.path().join(CONFIG_FILE),
r#"
[aliases]
copy = "path bar"
"#,
)
.unwrap();
let user_dir = TempDir::new().unwrap();
std::fs::write(
user_dir.path().join("aliases.toml"),
r#"
[aliases]
copy = "path foo"
ll = "list --format names"
"#,
)
.unwrap();
let user_path = user_dir.path().join("aliases.toml");
let resolved = aliases::load_layered(Some(repo_dir.path()), None, Some(&user_path)).unwrap();
assert_eq!(resolved.repo.get("copy").map(String::as_str), Some("path bar"));
assert_eq!(resolved.user.get("copy").map(String::as_str), Some("path foo"));
let argv = aliases::expand_argv(vec!["gwm".into(), "copy".into()], &resolved);
assert_eq!(argv, vec!["gwm", "path", "bar"]);
}
#[test]
fn resolved_aliases_user_only_when_no_repo_block() {
let repo_dir = TempDir::new().unwrap();
let user_dir = TempDir::new().unwrap();
std::fs::write(
user_dir.path().join("aliases.toml"),
r#"
[aliases]
ll = "list --format names"
"#,
)
.unwrap();
let user_path = user_dir.path().join("aliases.toml");
let resolved = aliases::load_layered(Some(repo_dir.path()), None, Some(&user_path)).unwrap();
assert!(resolved.repo.is_empty());
assert_eq!(resolved.user.get("ll").map(String::as_str), Some("list --format names"));
}
#[test]
fn resolved_aliases_user_missing_file_is_no_op() {
let repo_dir = TempDir::new().unwrap();
let resolved = aliases::load_layered(
Some(repo_dir.path()),
None,
Some(std::path::Path::new("/nope/aliases.toml")),
)
.unwrap();
assert!(resolved.repo.is_empty());
assert!(resolved.user.is_empty());
}
#[test]
fn resolved_aliases_user_rejects_shell_pipeline() {
let user_dir = TempDir::new().unwrap();
std::fs::write(
user_dir.path().join("aliases.toml"),
r#"
[aliases]
copy = "path | pbcopy"
"#,
)
.unwrap();
let user_path = user_dir.path().join("aliases.toml");
let err = aliases::load_layered(None, None, Some(&user_path)).unwrap_err();
match err {
GwmError::Config(msg) => {
assert!(msg.contains("copy"), "{msg}");
}
other => panic!("expected GwmError::Config, got {other:?}"),
}
}
#[test]
fn resolved_aliases_user_rejects_shadow_of_built_in() {
let user_dir = TempDir::new().unwrap();
std::fs::write(
user_dir.path().join("aliases.toml"),
r#"
[aliases]
list = "list --format names"
"#,
)
.unwrap();
let user_path = user_dir.path().join("aliases.toml");
let err = aliases::load_layered(None, None, Some(&user_path)).unwrap_err();
match err {
GwmError::Config(msg) => assert!(msg.contains("list"), "{msg}"),
other => panic!("expected GwmError::Config, got {other:?}"),
}
}
#[test]
fn expand_argv_no_match_passes_through_unchanged() {
let resolved = aliases::load_layered(None, None, None).unwrap();
let argv = vec!["gwm".into(), "list".into(), "--format".into(), "names".into()];
assert_eq!(aliases::expand_argv(argv.clone(), &resolved), argv);
}
#[test]
fn expand_argv_expands_repo_alias() {
let repo_dir = TempDir::new().unwrap();
std::fs::write(
repo_dir.path().join(CONFIG_FILE),
r#"
[aliases]
wip = "create feat 0 wip"
"#,
)
.unwrap();
let resolved = aliases::load_layered(Some(repo_dir.path()), None, None).unwrap();
let argv = vec!["gwm".into(), "wip".into()];
assert_eq!(
aliases::expand_argv(argv, &resolved),
vec!["gwm", "create", "feat", "0", "wip"]
);
}
#[test]
fn expand_argv_built_in_subcommand_always_wins() {
use gwm::aliases::{AliasEntry, ResolvedAliases};
use std::collections::BTreeMap;
let mut user = BTreeMap::new();
user.insert("list".to_string(), "create feat 0 wip".to_string());
let resolved = ResolvedAliases {
built_in: vec![AliasEntry {
name: "s",
expansion: "switch",
}],
repo: BTreeMap::new(),
user,
};
let argv = vec!["gwm".into(), "list".into()];
assert_eq!(aliases::expand_argv(argv.clone(), &resolved), argv);
}
#[test]
fn expand_argv_appends_user_trailing_arguments() {
let repo_dir = TempDir::new().unwrap();
std::fs::write(
repo_dir.path().join(CONFIG_FILE),
r#"
[aliases]
wip = "create feat 0 wip"
"#,
)
.unwrap();
let resolved = aliases::load_layered(Some(repo_dir.path()), None, None).unwrap();
let argv = vec!["gwm".into(), "wip".into(), "--no-bootstrap".into()];
assert_eq!(
aliases::expand_argv(argv, &resolved),
vec!["gwm", "create", "feat", "0", "wip", "--no-bootstrap"]
);
}
#[test]
fn expand_argv_only_first_position_is_substituted() {
let user_dir = TempDir::new().unwrap();
std::fs::write(
user_dir.path().join("aliases.toml"),
r#"
[aliases]
wip = "create feat 0 wip"
"#,
)
.unwrap();
let user_path = user_dir.path().join("aliases.toml");
let resolved = aliases::load_layered(None, None, Some(&user_path)).unwrap();
let argv = vec!["gwm".into(), "create".into(), "feat".into(), "86".into(), "wip".into()];
assert_eq!(aliases::expand_argv(argv.clone(), &resolved), argv);
}
#[test]
fn expand_argv_no_recursion_alias_pointing_at_alias() {
let repo_dir = TempDir::new().unwrap();
std::fs::write(
repo_dir.path().join(CONFIG_FILE),
r#"
[aliases]
wip = "ll"
ll = "list --format names"
"#,
)
.unwrap();
let resolved = aliases::load_layered(Some(repo_dir.path()), None, None).unwrap();
let argv = vec!["gwm".into(), "wip".into()];
assert_eq!(aliases::expand_argv(argv, &resolved), vec!["gwm", "ll"]);
}
#[test]
fn expand_argv_empty_argv_passes_through() {
let resolved = aliases::load_layered(None, None, None).unwrap();
let argv = vec!["gwm".into()];
assert_eq!(aliases::expand_argv(argv.clone(), &resolved), argv);
}
#[test]
fn expand_argv_global_flags_before_alias_are_preserved() {
let repo_dir = TempDir::new().unwrap();
std::fs::write(
repo_dir.path().join(CONFIG_FILE),
r#"
[aliases]
wip = "create feat 0 wip"
"#,
)
.unwrap();
let resolved = aliases::load_layered(Some(repo_dir.path()), None, None).unwrap();
let argv = vec!["gwm".into(), "--allow-bootstrap".into(), "wip".into()];
assert_eq!(
aliases::expand_argv(argv, &resolved),
vec!["gwm", "--allow-bootstrap", "create", "feat", "0", "wip"]
);
}
#[test]
fn expand_argv_os_matches_expand_argv_for_utf8_inputs() {
use std::ffi::OsString;
let repo_dir = tempfile::TempDir::new().unwrap();
std::fs::write(
repo_dir.path().join(".gwm.toml"),
r#"
[aliases]
wip = "create feat 0 wip"
"#,
)
.unwrap();
let resolved = aliases::load_layered(Some(repo_dir.path()), None, None).unwrap();
let argv: Vec<OsString> = vec!["gwm".into(), "--allow-bootstrap".into(), "wip".into()];
let expected: Vec<OsString> = vec![
"gwm".into(),
"--allow-bootstrap".into(),
"create".into(),
"feat".into(),
"0".into(),
"wip".into(),
];
assert_eq!(aliases::expand_argv_os(argv, &resolved), expected);
}
#[cfg(unix)]
#[test]
fn expand_argv_os_passes_non_utf8_token_through_unchanged() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let resolved = aliases::load_layered(None, None, None).unwrap();
let bad: OsString = OsString::from_vec(vec![0xff, 0xfe, 0x80]);
let argv: Vec<OsString> = vec!["gwm".into(), bad.clone()];
let out = aliases::expand_argv_os(argv.clone(), &resolved);
assert_eq!(out, argv, "non-UTF-8 token must flow through unchanged");
}
#[cfg(unix)]
#[test]
fn expand_argv_os_expands_alias_after_non_utf8_flag_value() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let resolved = aliases::load_layered(None, None, None).unwrap();
let bad: OsString = OsString::from_vec(vec![0xff]);
let argv: Vec<OsString> = vec!["gwm".into(), "--verbose".into(), bad.clone()];
let out = aliases::expand_argv_os(argv.clone(), &resolved);
assert_eq!(out, argv);
}
#[test]
fn built_in_aliases_constant_matches_clap_visible_aliases() {
let names: Vec<&str> = BUILT_IN_ALIASES.iter().map(|e| e.name).collect();
assert!(names.contains(&"cd"), "expected 'cd' in BUILT_IN_ALIASES: {names:?}");
assert!(names.contains(&"s"), "expected 's' in BUILT_IN_ALIASES: {names:?}");
}