#![allow(unused_imports)]
use std::collections::HashMap;
use std::path::PathBuf;
use crate::handlers::symlink::*;
use crate::handlers::{Handler, HandlerConfig, HandlerScope, HANDLER_SYMLINK};
use crate::operations::HandlerIntent;
use crate::packs::Pack;
use crate::paths::Pather;
use crate::rules::RuleMatch;
use super::{default_config, test_pather};
#[test]
fn force_home_top_level_file() {
let target = resolve_target("shell", "bashrc", &default_config(), &test_pather());
assert_eq!(target, PathBuf::from("/home/alice/.bashrc"));
}
#[test]
fn force_home_subdirectory_file() {
let target = resolve_target("net", "ssh/config", &default_config(), &test_pather());
assert_eq!(target, PathBuf::from("/home/alice/.ssh/config"));
}
#[test]
fn force_home_top_level_dir_wholesale() {
let target = resolve_target("net", "ssh", &default_config(), &test_pather());
assert_eq!(target, PathBuf::from("/home/alice/.ssh"));
}
#[test]
fn home_prefix_dir_escapes_pack_namespace() {
let config = HandlerConfig::default();
let target = resolve_target("misc", "_home/vim/vimrc", &config, &test_pather());
assert_eq!(target, PathBuf::from("/home/alice/.vim/vimrc"));
}
#[test]
fn xdg_prefix_dir_escapes_pack_namespace() {
let config = HandlerConfig::default();
let target = resolve_target(
"term-config",
"_xdg/ghostty/config",
&config,
&test_pather(),
);
assert_eq!(target, PathBuf::from("/home/alice/.config/ghostty/config"));
}
#[test]
fn app_prefix_routes_to_app_support_root() {
let config = HandlerConfig::default();
let target = resolve_target(
"macapps",
"_app/Code/User/settings.json",
&config,
&test_pather(),
);
assert_eq!(
target,
PathBuf::from("/home/alice/Library/Application Support/Code/User/settings.json")
);
}
#[test]
fn app_prefix_outranks_default() {
let config = HandlerConfig::default();
let target = resolve_target("Code", "_app/Code/x", &config, &test_pather());
assert_eq!(
target,
PathBuf::from("/home/alice/Library/Application Support/Code/x")
);
}
#[test]
fn lib_prefix_resolution_full_returns_skip_on_non_macos() {
let config = HandlerConfig::default();
let resolution = resolve_target_full(
"macapps",
"_lib/LaunchAgents/com.example.foo.plist",
&config,
&test_pather(),
);
if cfg!(target_os = "macos") {
match resolution {
Resolution::Path(p) => assert_eq!(
p,
PathBuf::from("/home/alice/Library/LaunchAgents/com.example.foo.plist")
),
Resolution::Skip { reason } => {
panic!("expected Path on macOS, got Skip({reason})")
}
}
} else {
assert!(
matches!(resolution, Resolution::Skip { .. }),
"_lib/ on non-macOS must skip; got {resolution:?}"
);
}
}
#[test]
fn force_app_routes_first_segment_to_app_support() {
let config = HandlerConfig {
force_app: vec!["Code".into()],
..HandlerConfig::default()
};
let target = resolve_target(
"macapps",
"Code/User/settings.json",
&config,
&test_pather(),
);
assert_eq!(
target,
PathBuf::from("/home/alice/Library/Application Support/Code/User/settings.json")
);
}
#[test]
fn force_app_is_case_sensitive() {
let config = HandlerConfig {
force_app: vec!["Code".into()],
..HandlerConfig::default()
};
let target = resolve_target("misc", "code/foo", &config, &test_pather());
assert_eq!(target, PathBuf::from("/home/alice/.config/misc/code/foo"));
}
#[test]
fn force_app_loses_to_explicit_app_prefix() {
let config = HandlerConfig {
force_app: vec!["Code".into()],
..HandlerConfig::default()
};
let target = resolve_target("misc", "_app/Code/x", &config, &test_pather());
assert_eq!(
target,
PathBuf::from("/home/alice/Library/Application Support/Code/x")
);
}
#[test]
fn app_alias_reroutes_default_rule() {
let mut aliases = std::collections::HashMap::new();
aliases.insert("vscode".into(), "Code".into());
let config = HandlerConfig {
app_aliases: aliases,
..HandlerConfig::default()
};
let target = resolve_target("vscode", "User/settings.json", &config, &test_pather());
assert_eq!(
target,
PathBuf::from("/home/alice/Library/Application Support/Code/User/settings.json")
);
}
#[test]
fn app_alias_loses_to_explicit_xdg_prefix() {
let mut aliases = std::collections::HashMap::new();
aliases.insert("vscode".into(), "Code".into());
let config = HandlerConfig {
app_aliases: aliases,
..HandlerConfig::default()
};
let target = resolve_target("vscode", "_xdg/Code/User/foo", &config, &test_pather());
assert_eq!(target, PathBuf::from("/home/alice/.config/Code/User/foo"));
}
#[test]
fn app_alias_loses_to_home_prefix() {
let mut aliases = std::collections::HashMap::new();
aliases.insert("vscode".into(), "Code".into());
let config = HandlerConfig {
app_aliases: aliases,
..HandlerConfig::default()
};
let target = resolve_target("vscode", "home.editorconfig", &config, &test_pather());
assert_eq!(target, PathBuf::from("/home/alice/.editorconfig"));
}
#[test]
fn app_alias_uses_pack_display_name() {
let mut aliases = std::collections::HashMap::new();
aliases.insert("vscode".into(), "Code".into());
let config = HandlerConfig {
app_aliases: aliases,
..HandlerConfig::default()
};
let target = resolve_target("010-vscode", "settings.json", &config, &test_pather());
assert_eq!(
target,
PathBuf::from("/home/alice/Library/Application Support/Code/settings.json")
);
}
#[test]
fn force_app_outranks_app_alias() {
let mut aliases = std::collections::HashMap::new();
aliases.insert("anything".into(), "AliasedFolder".into());
let config = HandlerConfig {
force_app: vec!["Cursor".into()],
app_aliases: aliases,
..HandlerConfig::default()
};
let target = resolve_target("anything", "Cursor/x", &config, &test_pather());
assert_eq!(
target,
PathBuf::from("/home/alice/Library/Application Support/Cursor/x")
);
}
#[test]
fn home_prefix_routes_top_level_file_to_home() {
let config = HandlerConfig::default();
let target = resolve_target("git", "home.gitconfig", &config, &test_pather());
assert_eq!(target, PathBuf::from("/home/alice/.gitconfig"));
}
#[test]
fn home_prefix_works_even_when_pack_not_force_home() {
let config = HandlerConfig::default();
let target = resolve_target("misc", "home.vimrc", &config, &test_pather());
assert_eq!(target, PathBuf::from("/home/alice/.vimrc"));
}
#[test]
fn home_prefix_not_applied_to_subdirs() {
let config = HandlerConfig::default();
let target = resolve_target("misc", "subdir/home.conf", &config, &test_pather());
assert_eq!(
target,
PathBuf::from("/home/alice/.config/misc/subdir/home.conf")
);
}
#[test]
fn strip_file_prefix_unit() {
assert_eq!(
strip_file_prefix("home.bashrc"),
Some((FilePrefix::Home, "bashrc"))
);
assert_eq!(
strip_file_prefix("home.vimrc"),
Some((FilePrefix::Home, "vimrc"))
);
assert_eq!(
strip_file_prefix("app.config"),
Some((FilePrefix::App, "config"))
);
assert_eq!(
strip_file_prefix("xdg.mimeapps.list"),
Some((FilePrefix::Xdg, "mimeapps.list"))
);
assert_eq!(
strip_file_prefix("lib.com.example.plist"),
Some((FilePrefix::Lib, "com.example.plist"))
);
assert_eq!(strip_file_prefix("vimrc"), None);
assert_eq!(strip_file_prefix(".bashrc"), None);
assert_eq!(strip_file_prefix("sub/home.conf"), None);
assert_eq!(strip_file_prefix("sub/app.json"), None);
assert_eq!(strip_file_prefix("home."), None);
assert_eq!(strip_file_prefix("app."), None);
assert_eq!(strip_file_prefix("xdg."), None);
assert_eq!(strip_file_prefix("lib."), None);
}
#[test]
fn literal_home_dot_filename_does_not_target_home_root() {
let config = HandlerConfig::default();
let target = resolve_target("misc", "home.", &config, &test_pather());
assert_eq!(target, PathBuf::from("/home/alice/.config/misc/home."));
}
#[test]
fn literal_bare_file_prefix_filenames_do_not_target_root() {
let config = HandlerConfig::default();
for name in ["app.", "xdg.", "lib."] {
let target = resolve_target("misc", name, &config, &test_pather());
assert_eq!(
target,
PathBuf::from(format!("/home/alice/.config/misc/{name}")),
"bare `{name}` should fall through to default rule"
);
}
}
#[test]
fn app_prefix_routes_top_level_file_to_app_support_root() {
let config = HandlerConfig::default();
let target = resolve_target("vscode", "app.settings.json", &config, &test_pather());
assert_eq!(
target,
PathBuf::from("/home/alice/Library/Application Support/settings.json")
);
}
#[test]
fn xdg_prefix_routes_top_level_file_to_xdg_root() {
let config = HandlerConfig::default();
let target = resolve_target("desktop", "xdg.mimeapps.list", &config, &test_pather());
assert_eq!(target, PathBuf::from("/home/alice/.config/mimeapps.list"));
}
#[test]
fn lib_prefix_routes_top_level_file_to_library_on_macos() {
let config = HandlerConfig::default();
let resolution = resolve_target_full(
"macapps",
"lib.com.example.foo.plist",
&config,
&test_pather(),
);
if cfg!(target_os = "macos") {
match resolution {
Resolution::Path(p) => assert_eq!(
p,
PathBuf::from("/home/alice/Library/com.example.foo.plist")
),
Resolution::Skip { reason } => {
panic!("expected Path on macOS, got Skip({reason})")
}
}
} else {
assert!(
matches!(resolution, Resolution::Skip { .. }),
"lib.X on non-macOS must skip; got {resolution:?}"
);
}
}
#[test]
fn file_prefixes_only_apply_at_top_level() {
let config = HandlerConfig::default();
for name in ["app.settings.json", "xdg.mimeapps.list", "lib.foo.plist"] {
let nested = format!("subdir/{name}");
let target = resolve_target("misc", &nested, &config, &test_pather());
assert_eq!(
target,
PathBuf::from(format!("/home/alice/.config/misc/{nested}")),
"nested `{name}` must keep prefix literal"
);
}
}
#[test]
fn lib_file_prefix_emits_warning_on_non_macos() {
let env = crate::testing::TempEnvironment::builder()
.pack("macapps")
.file("lib.com.example.foo.plist", "# stub plist")
.done()
.build();
let m = RuleMatch {
relative_path: PathBuf::from("lib.com.example.foo.plist"),
absolute_path: env.dotfiles_root.join("macapps/lib.com.example.foo.plist"),
pack: "macapps".into(),
handler: HANDLER_SYMLINK.into(),
is_dir: false,
options: std::collections::HashMap::new(),
preprocessor_source: None,
rendered_bytes: None,
};
let handler = SymlinkHandler;
let config = HandlerConfig::default();
let warnings =
handler.warnings_for_matches(std::slice::from_ref(&m), &config, env.paths.as_ref());
if cfg!(target_os = "macos") {
assert!(
warnings.is_empty(),
"lib.X should not warn on macOS; got {warnings:?}"
);
let intents = handler
.to_intents(&[m], &config, env.paths.as_ref(), env.fs.as_ref())
.unwrap();
assert_eq!(intents.len(), 1);
} else {
assert_eq!(warnings.len(), 1, "expected one warning, got {warnings:?}");
assert!(
warnings[0].contains("macOS-only path"),
"warning text should mention macOS-only: {warnings:?}"
);
let intents = handler
.to_intents(&[m], &config, env.paths.as_ref(), env.fs.as_ref())
.unwrap();
assert!(
intents.is_empty(),
"lib.X on non-macOS must not emit Link intents: {intents:?}"
);
}
}