#![allow(unused_imports)]
use std::sync::Arc;
use crate::commands;
use crate::config::ConfigManager;
use crate::datastore::{CommandOutput, CommandRunner, FilesystemDataStore};
use crate::fs::Fs;
use crate::packs::orchestration::ExecutionContext;
use crate::paths::Pather;
use crate::render;
use crate::testing::TempEnvironment;
use crate::Result;
use standout_render::OutputMode;
use super::support::{make_ctx, make_ctx_with_runner, CannedRunner};
#[test]
fn status_does_not_write_to_datastore() {
let env = TempEnvironment::builder()
.pack("app")
.file("config.toml.tmpl", "name = {{ name }}")
.config("[preprocessor.template.vars]\nname = \"Alice\"\n")
.done()
.build();
let ctx = make_ctx(&env);
commands::up::up(None, &ctx).unwrap();
let snapshot = snapshot_dir_contents(&env, env.paths.data_dir());
commands::status::status(None, &ctx).unwrap();
commands::status::status(None, &ctx).unwrap();
let after = snapshot_dir_contents(&env, env.paths.data_dir());
assert_eq!(
snapshot, after,
"status must be byte-identical to the post-up snapshot — \
no datastore writes allowed"
);
}
#[test]
fn up_dry_run_does_not_write_to_datastore() {
let env = TempEnvironment::builder()
.pack("app")
.file("config.toml.tmpl", "name = {{ name }}")
.config("[preprocessor.template.vars]\nname = \"Alice\"\n")
.done()
.build();
let ctx = make_ctx(&env);
commands::up::up(None, &ctx).unwrap();
let snapshot = snapshot_dir_contents(&env, env.paths.data_dir());
let mut dry_ctx = make_ctx(&env);
dry_ctx.dry_run = true;
commands::up::up(None, &dry_ctx).unwrap();
let after = snapshot_dir_contents(&env, env.paths.data_dir());
assert_eq!(
snapshot, after,
"dry-run must be byte-identical to the post-up snapshot"
);
}
#[test]
fn install_template_dry_run_emits_correct_sentinel_without_writing_rendered_file() {
let env = TempEnvironment::builder()
.pack("app")
.file("install.sh.tmpl", "#!/bin/sh\necho hello {{ name }}")
.config("[preprocessor.template.vars]\nname = \"Alice\"\n")
.done()
.build();
let mut ctx = make_ctx(&env);
ctx.no_provision = false;
commands::up::up(None, &ctx).unwrap();
let active_intents = crate::packs::orchestration::collect_pack_intents(
&crate::packs::Pack::new(
"app".into(),
env.dotfiles_root.join("app"),
ctx.config_manager
.config_for_pack(&env.dotfiles_root.join("app"))
.unwrap()
.to_handler_config(),
),
&ctx,
)
.unwrap();
let active_sentinel = active_intents
.iter()
.find_map(|i| match i {
crate::operations::HandlerIntent::Run { sentinel, .. } => Some(sentinel.clone()),
_ => None,
})
.expect("active path must produce a Run intent for install.sh");
let rendered_path = env
.paths
.handler_data_dir("app", "preprocessed")
.join("install.sh");
assert!(
ctx.fs.exists(&rendered_path),
"active up must have written the rendered file"
);
let rendered_before = ctx.fs.read_file(&rendered_path).unwrap();
let mut dry_ctx = make_ctx(&env);
dry_ctx.no_provision = false;
dry_ctx.dry_run = true;
let plan = crate::packs::orchestration::plan_pack(
&crate::packs::Pack::new(
"app".into(),
env.dotfiles_root.join("app"),
dry_ctx
.config_manager
.config_for_pack(&env.dotfiles_root.join("app"))
.unwrap()
.to_handler_config(),
),
&dry_ctx,
crate::preprocessing::PreprocessMode::Passive,
)
.unwrap();
let dry_sentinel = plan
.intents
.iter()
.find_map(|i| match i {
crate::operations::HandlerIntent::Run { sentinel, .. } => Some(sentinel.clone()),
_ => None,
})
.expect("passive path must produce a Run intent for install.sh");
assert_eq!(
active_sentinel, dry_sentinel,
"passive sentinel must match active — same rendered bytes either way"
);
let rendered_after = ctx.fs.read_file(&rendered_path).unwrap();
assert_eq!(
rendered_before, rendered_after,
"passive must not rewrite the rendered file"
);
}
#[test]
fn up_dry_run_first_time_pack_with_install_template_does_not_error() {
let env = TempEnvironment::builder()
.pack("setup")
.file("install.sh.tmpl", "#!/bin/sh\necho hello {{ name }}")
.file("Brewfile.tmpl", "brew '{{ pkg }}'")
.config("[preprocessor.template.vars]\nname = \"Alice\"\npkg = \"jq\"\n")
.done()
.build();
let mut dry_ctx = make_ctx(&env);
dry_ctx.no_provision = false;
dry_ctx.dry_run = true;
let result = commands::up::up(None, &dry_ctx).unwrap();
assert!(result.dry_run);
}
#[test]
fn passive_first_time_pack_surfaces_pending_placeholder() {
let env = TempEnvironment::builder()
.pack("app")
.file("greet.tmpl", "hello {{ name }}")
.config("[preprocessor.template.vars]\nname = \"Alice\"\n")
.done()
.build();
let ctx = make_ctx(&env);
let result = commands::status::status(None, &ctx).unwrap();
let files = &result.packs[0].files;
assert_eq!(files.len(), 1, "should surface the templated entry");
assert_eq!(
files[0].name, "greet",
"stripped name (not source filename)"
);
assert_eq!(
files[0].status, "pending",
"first-time template before any up: pending"
);
}
fn snapshot_dir_contents(
env: &crate::testing::TempEnvironment,
root: &std::path::Path,
) -> std::collections::BTreeMap<std::path::PathBuf, Option<Vec<u8>>> {
use std::collections::BTreeMap;
let mut out = BTreeMap::new();
if !env.fs.exists(root) {
return out;
}
let mut stack: Vec<std::path::PathBuf> = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
let entries = env
.fs
.read_dir(&dir)
.unwrap_or_else(|e| panic!("snapshot_dir_contents: read_dir({dir:?}): {e}"));
for entry in entries {
if entry.is_dir {
out.insert(entry.path.clone(), None);
stack.push(entry.path);
} else {
let bytes = env.fs.read_file(&entry.path).unwrap_or_else(|e| {
panic!("snapshot_dir_contents: read_file({:?}): {e}", entry.path)
});
out.insert(entry.path, Some(bytes));
}
}
}
out
}
#[test]
fn up_halts_on_cross_pack_symlink_conflict() {
let env = TempEnvironment::builder()
.pack("pack-a")
.file("home.aliases", "alias a=1")
.done()
.pack("pack-b")
.file("home.aliases", "alias b=2")
.done()
.build();
let ctx = make_ctx(&env);
let err = commands::up::up(None, &ctx).unwrap_err();
assert!(
matches!(err, crate::DodotError::CrossPackConflict { .. }),
"expected CrossPackConflict, got: {err}"
);
let msg = err.to_string();
assert!(msg.contains("pack-a"), "msg: {msg}");
assert!(msg.contains("pack-b"), "msg: {msg}");
assert!(msg.contains(".aliases"), "msg: {msg}");
}
#[test]
fn up_halts_no_partial_deployment_on_conflict() {
let env = TempEnvironment::builder()
.pack("conflict-a")
.file("home.aliases", "a")
.done()
.pack("conflict-b")
.file("home.aliases", "b")
.done()
.pack("innocent")
.file("vimrc", "set nocompatible")
.done()
.build();
let ctx = make_ctx(&env);
let _err = commands::up::up(None, &ctx).unwrap_err();
env.assert_no_handler_state("innocent", "symlink");
env.assert_no_handler_state("conflict-a", "symlink");
env.assert_no_handler_state("conflict-b", "symlink");
}
#[test]
fn up_force_does_not_override_cross_pack_conflict() {
let env = TempEnvironment::builder()
.pack("pack-a")
.file("home.aliases", "a")
.done()
.pack("pack-b")
.file("home.aliases", "b")
.done()
.build();
let mut ctx = make_ctx(&env);
ctx.force = true;
let err = commands::up::up(None, &ctx).unwrap_err();
assert!(
matches!(err, crate::DodotError::CrossPackConflict { .. }),
"force should NOT override cross-pack conflict, got: {err}"
);
assert!(
err.to_string().contains("--force does not override"),
"msg: {}",
err
);
}
#[test]
fn up_dry_run_still_detects_cross_pack_conflict() {
let env = TempEnvironment::builder()
.pack("a")
.file("bashrc", "a")
.done()
.pack("b")
.file("bashrc", "b")
.done()
.build();
let mut ctx = make_ctx(&env);
ctx.dry_run = true;
let err = commands::up::up(None, &ctx).unwrap_err();
assert!(
matches!(err, crate::DodotError::CrossPackConflict { .. }),
"dry-run should still detect conflicts, got: {err}"
);
}
#[test]
fn up_with_cross_pack_conflict_renders_full_status_view() {
let env = TempEnvironment::builder()
.pack("pack-a")
.file("home.aliases", "alias a=1")
.done()
.pack("pack-b")
.file("home.aliases", "alias b=2")
.done()
.pack("innocent")
.file("home.vimrc", "set nocompatible")
.done()
.build();
let ctx = make_ctx(&env);
let result = commands::up::up_or_status_for_conflict(None, &ctx)
.expect("status fallback should produce Ok on cross-pack conflict");
assert_eq!(
result.message.as_deref(),
Some("Cross-pack conflicts prevent deployment."),
"got message: {:?}",
result.message
);
assert!(
!result.packs.is_empty(),
"up-with-conflict must render pack rows, not a bare conflicts dump"
);
let pack_names: Vec<&str> = result.packs.iter().map(|p| p.name.as_str()).collect();
assert!(
pack_names.contains(&"pack-a"),
"expected pack-a in listing, got: {:?}",
pack_names
);
assert!(
pack_names.contains(&"pack-b"),
"expected pack-b in listing, got: {:?}",
pack_names
);
assert!(
pack_names.contains(&"innocent"),
"expected innocent pack in listing, got: {:?}",
pack_names
);
assert!(
!result.conflicts.is_empty(),
"expected conflicts section to be populated"
);
let conflict = &result.conflicts[0];
assert!(
conflict.target.contains(".aliases"),
"conflict target should reference .aliases, got: {}",
conflict.target
);
for pack in &result.packs {
for file in &pack.files {
assert_ne!(
file.status, "deployed",
"{}::{} should not be deployed when conflict blocks up, got: {} ({})",
pack.name, file.name, file.status, file.status_label
);
}
}
}
#[test]
fn up_no_conflict_when_different_target_files() {
let env = TempEnvironment::builder()
.pack("vim")
.file("vimrc", "set nocompatible")
.done()
.pack("git")
.file("gitconfig", "[user]\n name = test")
.done()
.build();
let ctx = make_ctx(&env);
let result = commands::up::up(None, &ctx).unwrap();
assert_eq!(result.message.as_deref(), Some("Packs deployed."));
}
#[test]
fn up_no_conflict_within_same_pack() {
let env = TempEnvironment::builder()
.pack("shell")
.file("bashrc", "# bash")
.file("zshrc", "# zsh")
.done()
.build();
let ctx = make_ctx(&env);
let result = commands::up::up(None, &ctx).unwrap();
assert_eq!(result.message.as_deref(), Some("Packs deployed."));
}
#[test]
fn up_conflict_via_config_mapping() {
let env = TempEnvironment::builder()
.pack("pack-a")
.file("settings", "a")
.config("[symlink]\ntargets = { settings = \"myapp/settings.toml\" }")
.done()
.pack("pack-b")
.file("config", "b")
.config("[symlink]\ntargets = { config = \"myapp/settings.toml\" }")
.done()
.build();
let ctx = make_ctx(&env);
let err = commands::up::up(None, &ctx).unwrap_err();
assert!(
matches!(err, crate::DodotError::CrossPackConflict { .. }),
"expected CrossPackConflict for config mapping collision, got: {err}"
);
let msg = err.to_string();
assert!(
msg.contains("myapp/settings.toml"),
"should mention the conflicting target: {msg}"
);
}
#[test]
fn up_conflict_via_home_prefix() {
let env = TempEnvironment::builder()
.pack("pack-a")
.file("_home/vim/vimrc", "a")
.done()
.pack("pack-b")
.file("vim/vimrc", "b")
.done()
.build();
let ctx = make_ctx(&env);
let result = commands::up::up(None, &ctx).unwrap();
assert_eq!(
result.message.as_deref(),
Some("Packs deployed."),
"different targets should not conflict"
);
}
#[test]
fn up_conflict_two_packs_same_home_prefix_target() {
let env = TempEnvironment::builder()
.pack("pack-a")
.file("home.bashrc", "# a")
.done()
.pack("pack-b")
.file("home.bashrc", "# b")
.done()
.build();
let ctx = make_ctx(&env);
let err = commands::up::up(None, &ctx).unwrap_err();
assert!(
matches!(err, crate::DodotError::CrossPackConflict { .. }),
"both targeting ~/.bashrc should conflict, got: {err}"
);
}
#[test]
fn up_filtered_packs_only_checks_filtered_subset() {
let env = TempEnvironment::builder()
.pack("pack-a")
.file("home.aliases", "a")
.done()
.pack("pack-b")
.file("home.aliases", "b")
.done()
.build();
let ctx = make_ctx(&env);
let filter = vec!["pack-a".into()];
let result = commands::up::up(Some(&filter), &ctx).unwrap();
assert_eq!(result.message.as_deref(), Some("Packs deployed."));
assert_eq!(result.packs.len(), 1);
assert_eq!(result.packs[0].name, "pack-a");
}
#[test]
fn up_same_name_shell_scripts_are_not_conflicts() {
let env = TempEnvironment::builder()
.pack("vim")
.file("aliases.sh", "alias vi=vim")
.done()
.pack("git")
.file("aliases.sh", "alias g=git")
.done()
.build();
let ctx = make_ctx(&env);
let result = commands::up::up(None, &ctx).unwrap();
assert_eq!(result.message.as_deref(), Some("Packs deployed."));
}
#[test]
fn up_path_dirs_with_different_executables_ok() {
let env = TempEnvironment::builder()
.pack("tools-a")
.file("bin/tool-a", "#!/bin/sh")
.done()
.pack("tools-b")
.file("bin/tool-b", "#!/bin/sh")
.done()
.build();
let ctx = make_ctx(&env);
let result = commands::up::up(None, &ctx).unwrap();
assert_eq!(result.message.as_deref(), Some("Packs deployed."));
}
#[test]
fn up_path_dirs_with_same_executable_name_conflicts() {
let env = TempEnvironment::builder()
.pack("tools-a")
.file("bin/tool", "#!/bin/sh\necho a")
.done()
.pack("tools-b")
.file("bin/tool", "#!/bin/sh\necho b")
.done()
.build();
let ctx = make_ctx(&env);
let err = commands::up::up(None, &ctx).unwrap_err();
assert!(
matches!(err, crate::DodotError::CrossPackConflict { .. }),
"same-name executables across packs should conflict: {err}"
);
let msg = err.to_string();
assert!(msg.contains("tool"), "should mention the executable: {msg}");
assert!(msg.contains("tools-a"), "should mention pack a: {msg}");
assert!(msg.contains("tools-b"), "should mention pack b: {msg}");
}
#[test]
fn up_no_cross_handler_conflict() {
let env = TempEnvironment::builder()
.pack("vim")
.file("aliases.sh", "alias vi=vim")
.done()
.pack("git")
.file("gitconfig", "[user]\n name = test")
.done()
.build();
let ctx = make_ctx(&env);
let result = commands::up::up(None, &ctx).unwrap();
assert_eq!(result.message.as_deref(), Some("Packs deployed."));
}
#[test]
fn up_three_packs_partial_conflict() {
let env = TempEnvironment::builder()
.pack("a")
.file("home.aliases", "a")
.done()
.pack("b")
.file("home.aliases", "b")
.done()
.pack("c")
.file("gitconfig", "c")
.done()
.build();
let ctx = make_ctx(&env);
let err = commands::up::up(None, &ctx).unwrap_err();
assert!(
matches!(err, crate::DodotError::CrossPackConflict { .. }),
"should detect the conflict even if not all packs are involved"
);
env.assert_no_handler_state("a", "symlink");
env.assert_no_handler_state("b", "symlink");
env.assert_no_handler_state("c", "symlink");
}
#[test]
fn up_error_message_includes_all_conflict_details() {
let env = TempEnvironment::builder()
.pack("alpha")
.file("home.aliases", "a")
.done()
.pack("beta")
.file("home.aliases", "b")
.done()
.build();
let ctx = make_ctx(&env);
let err = commands::up::up(None, &ctx).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("alpha"), "msg: {msg}");
assert!(msg.contains("beta"), "msg: {msg}");
assert!(msg.contains("symlink"), "msg: {msg}");
assert!(msg.contains(".aliases"), "msg: {msg}");
}
#[test]
fn status_warns_on_potential_cross_pack_conflict() {
let env = TempEnvironment::builder()
.pack("pack-a")
.file("home.aliases", "a")
.done()
.pack("pack-b")
.file("home.aliases", "b")
.done()
.build();
let ctx = make_ctx(&env);
let result = commands::status::status(None, &ctx).unwrap();
assert_eq!(result.conflicts.len(), 1, "should detect one conflict");
let c = &result.conflicts[0];
assert_eq!(c.kind, "symlink");
let packs: Vec<&str> = c.claimants.iter().map(|cl| cl.pack.as_str()).collect();
assert!(packs.contains(&"pack-a"), "claimants: {:?}", c.claimants);
assert!(packs.contains(&"pack-b"), "claimants: {:?}", c.claimants);
}
#[test]
fn status_no_warnings_without_conflicts() {
let env = TempEnvironment::builder()
.pack("vim")
.file("vimrc", "set nocompatible")
.done()
.pack("git")
.file("gitconfig", "[user]\n name = test")
.done()
.build();
let ctx = make_ctx(&env);
let result = commands::status::status(None, &ctx).unwrap();
assert!(
result.warnings.is_empty(),
"no warnings expected, got: {:?}",
result.warnings
);
assert!(
result.conflicts.is_empty(),
"no conflicts expected, got: {:?}",
result.conflicts
);
}
#[test]
fn status_shows_conflict_even_when_not_deployed() {
let env = TempEnvironment::builder()
.pack("a")
.file("bashrc", "a")
.done()
.pack("b")
.file("bashrc", "b")
.done()
.build();
let ctx = make_ctx(&env);
let result = commands::status::status(None, &ctx).unwrap();
for pack in &result.packs {
for file in &pack.files {
assert_eq!(file.status, "pending");
}
}
assert!(
!result.conflicts.is_empty(),
"should flag potential conflict even when undeployed"
);
}
#[test]
fn status_filtered_to_one_pack_no_conflict_warning() {
let env = TempEnvironment::builder()
.pack("a")
.file("home.aliases", "a")
.done()
.pack("b")
.file("home.aliases", "b")
.done()
.build();
let ctx = make_ctx(&env);
let filter = vec!["a".into()];
let result = commands::status::status(Some(&filter), &ctx).unwrap();
assert!(
result.conflicts.is_empty(),
"single-pack filter should not produce cross-pack conflicts"
);
}
#[test]
fn status_conflict_with_config_mapping() {
let env = TempEnvironment::builder()
.pack("pack-a")
.file("settings", "a")
.config("[symlink]\ntargets = { settings = \"myapp/settings.toml\" }")
.done()
.pack("pack-b")
.file("config", "b")
.config("[symlink]\ntargets = { config = \"myapp/settings.toml\" }")
.done()
.build();
let ctx = make_ctx(&env);
let result = commands::status::status(None, &ctx).unwrap();
assert_eq!(
result.conflicts.len(),
1,
"config mapping collision should surface one conflict"
);
assert!(
result.conflicts[0].target.contains("settings.toml"),
"should mention the conflicting target: {:?}",
result.conflicts[0]
);
}
#[test]
fn pack_os_inactive_pack_surfaces_in_status() {
let env = TempEnvironment::builder()
.pack("vim")
.file("vimrc", "x")
.done()
.pack("mac-only")
.file("install.sh", "#!/bin/sh\necho mac")
.config("[pack]\nos = [\"nonexistent-os\"]")
.done()
.build();
let ctx = make_ctx(&env);
let result = commands::status::status(None, &ctx).unwrap();
let active_pack_names: Vec<&str> = result.packs.iter().map(|p| p.name.as_str()).collect();
assert_eq!(active_pack_names, vec!["vim"]);
assert_eq!(
result.inactive_packs.len(),
1,
"{:?}",
result.inactive_packs
);
let entry = &result.inactive_packs[0];
assert!(entry.starts_with("mac-only"), "{entry}");
assert!(entry.contains("os=nonexistent-os"), "{entry}");
assert!(entry.contains("current="), "{entry}");
let output = render::render("pack-status", &result, OutputMode::Text).unwrap();
assert!(output.contains("Inactive on this OS"), "output: {output}");
assert!(output.contains("mac-only"), "output: {output}");
}
#[test]
fn pack_os_active_pack_runs_normally() {
let env = TempEnvironment::builder()
.pack("portable")
.file("vimrc", "x")
.config("[pack]\nos = [\"darwin\", \"linux\", \"windows\"]")
.done()
.build();
let ctx = make_ctx(&env);
let result = commands::status::status(None, &ctx).unwrap();
let active_pack_names: Vec<&str> = result.packs.iter().map(|p| p.name.as_str()).collect();
assert_eq!(active_pack_names, vec!["portable"]);
assert!(result.inactive_packs.is_empty());
}
#[test]
fn pack_os_macos_alias_matches_darwin_target() {
if !cfg!(target_os = "macos") {
return;
}
let env = TempEnvironment::builder()
.pack("mac")
.file("vimrc", "x")
.config("[pack]\nos = [\"macos\"]")
.done()
.build();
let ctx = make_ctx(&env);
let result = commands::status::status(None, &ctx).unwrap();
let names: Vec<&str> = result.packs.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names, vec!["mac"]);
assert!(result.inactive_packs.is_empty());
}
#[test]
fn pack_os_inactive_pack_emits_no_operations_in_up() {
let env = TempEnvironment::builder()
.pack("mac-only")
.file("Brewfile", "brew \"ripgrep\"")
.config("[pack]\nos = [\"nonexistent-os\"]")
.done()
.build();
let ctx = make_ctx(&env);
let result = commands::up::up(None, &ctx).unwrap();
assert!(result.packs.is_empty(), "packs: {:?}", result.packs);
}
#[test]
fn adopt_only_os_wraps_file_in_gate_dir() {
let env = TempEnvironment::builder()
.pack("vim")
.file("placeholder", "")
.done()
.home_file(".vimrc", "set nocompatible")
.build();
let ctx = make_ctx(&env);
let source = env.home.join(".vimrc");
commands::adopt::adopt(
Some("vim"),
std::slice::from_ref(&source),
false,
false,
false,
Some("darwin"),
&ctx,
)
.unwrap();
env.assert_regular_file(
&env.dotfiles_root.join("vim/_darwin/home.vimrc"),
"set nocompatible",
);
assert!(env.fs.is_symlink(&source));
}
#[test]
fn adopt_only_os_unknown_label_errors() {
let env = TempEnvironment::builder()
.pack("vim")
.file("placeholder", "")
.done()
.home_file(".vimrc", "x")
.build();
let ctx = make_ctx(&env);
let source = env.home.join(".vimrc");
let err = commands::adopt::adopt(
Some("vim"),
std::slice::from_ref(&source),
false,
false,
false,
Some("nonexistent-label"),
&ctx,
)
.unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("nonexistent-label"), "missing label: {msg}");
assert!(msg.contains("--only-os"), "missing flag: {msg}");
}
#[test]
fn adopt_only_os_user_defined_label_works() {
let env = TempEnvironment::builder()
.pack("vim")
.file("placeholder", "")
.done()
.home_file(".vimrc", "x")
.build();
env.fs
.write_file(
&env.dotfiles_root.join(".dodot.toml"),
b"[gates]\nlaptop = { hostname = \"mbp\" }\n",
)
.unwrap();
let ctx = make_ctx(&env);
let source = env.home.join(".vimrc");
commands::adopt::adopt(
Some("vim"),
std::slice::from_ref(&source),
false,
false,
false,
Some("laptop"),
&ctx,
)
.unwrap();
env.assert_regular_file(&env.dotfiles_root.join("vim/_laptop/home.vimrc"), "x");
}
#[test]
fn gate_failed_template_does_not_render_at_up() {
let gated = if cfg!(target_os = "macos") {
"linux"
} else if cfg!(target_os = "linux") {
"darwin"
} else {
return; };
let template_name = format!("aliases._{gated}.sh.tmpl");
let env = TempEnvironment::builder()
.pack("p")
.file(&template_name, "alias x={{ undefined_variable }}")
.file("home.profile", "export PATH=$PATH:~/.local/bin")
.done()
.build();
let ctx = make_ctx(&env);
commands::up::up(None, &ctx).unwrap();
let profile_link = env.home.join(".profile");
assert!(
env.fs.exists(&profile_link),
"co-located plain file was not deployed; pack planning likely failed \
because the gated template still reached the template engine: {profile_link:?}"
);
let baseline_dir = ctx.paths.cache_dir().join("preprocessor/p/template");
if env.fs.exists(&baseline_dir) {
let baselines = env.fs.read_dir(&baseline_dir).unwrap_or_default();
assert!(
baselines.is_empty(),
"preprocessor wrote {} baseline file(s) for a gated-out template: {:?}",
baselines.len(),
baselines.iter().map(|e| e.name.clone()).collect::<Vec<_>>()
);
}
let preprocessed = ctx.paths.data_dir().join("packs/p/preprocessed/aliases.sh");
assert!(
!env.fs.exists(&preprocessed),
"gated-out template was rendered to datastore at {preprocessed:?}"
);
let shell_link = ctx.paths.data_dir().join("packs/p/shell/aliases.sh");
assert!(
!env.fs.exists(&shell_link),
"gated-out template surfaced as a shell-stage entry at {shell_link:?}"
);
}
#[test]
fn up_catches_mappings_gates_filename_conflict() {
let env = TempEnvironment::builder()
.pack("p")
.file("install._darwin.sh", "echo x")
.config("[mappings.gates]\n\"install._darwin.sh\" = \"linux\"\n")
.done()
.build();
let ctx = make_ctx(&env);
let err = commands::up::up(None, &ctx).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("gate-routing conflict"), "msg: {msg}");
assert!(msg.contains("install._darwin.sh"), "msg: {msg}");
}
#[test]
fn up_rejects_invalid_mappings_gates_glob() {
let env = TempEnvironment::builder()
.pack("p")
.file("vimrc", "x")
.config("[mappings.gates]\n\"[unclosed\" = \"darwin\"\n")
.done()
.build();
let ctx = make_ctx(&env);
let err = commands::up::up(None, &ctx).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("invalid `[mappings.gates]` glob"),
"msg: {msg}"
);
}
#[test]
fn status_surfaces_gated_template_under_original_name() {
let gated = if cfg!(target_os = "macos") {
"linux"
} else if cfg!(target_os = "linux") {
"darwin"
} else {
return;
};
let template_name = format!("aliases._{gated}.sh.tmpl");
let env = TempEnvironment::builder()
.pack("p")
.file(&template_name, "alias x=y\n")
.done()
.build();
let ctx = make_ctx(&env);
let result = commands::status::status(None, &ctx).unwrap();
assert_eq!(result.packs.len(), 1);
let files = &result.packs[0].files;
assert_eq!(files.len(), 1, "files: {files:?}");
let row = &files[0];
assert_eq!(
row.name, template_name,
"expected source filename in row, not a preprocessed virtual name"
);
assert_eq!(row.handler, "gate", "row.handler: {}", row.handler);
}
#[test]
fn up_skips_mappings_gated_template() {
let gated = if cfg!(target_os = "macos") {
"linux"
} else if cfg!(target_os = "linux") {
"darwin"
} else {
return;
};
let env = TempEnvironment::builder()
.pack("p")
.file("aliases.sh.tmpl", "alias x={{ undefined_variable }}")
.file("home.profile", "export PATH=$PATH:~/.local/bin")
.config(&format!(
"[mappings.gates]\n\"aliases.sh.tmpl\" = \"{gated}\"\n"
))
.done()
.build();
let ctx = make_ctx(&env);
commands::up::up(None, &ctx).unwrap();
let profile_link = env.home.join(".profile");
assert!(
env.fs.exists(&profile_link),
"co-located plain file was not deployed; mapping-gated template \
likely reached the engine: {profile_link:?}"
);
let baseline_dir = ctx.paths.cache_dir().join("preprocessor/p/template");
if env.fs.exists(&baseline_dir) {
let baselines = env.fs.read_dir(&baseline_dir).unwrap_or_default();
assert!(
baselines.is_empty(),
"preprocessor wrote {} baseline file(s) for a mapping-gated template: {:?}",
baselines.len(),
baselines.iter().map(|e| e.name.clone()).collect::<Vec<_>>()
);
}
}