use standout_render::OutputMode;
use super::support::make_ctx;
use crate::commands::{status, up};
use crate::fs::Fs;
use crate::paths::Pather;
use crate::render;
use crate::shell::activation;
use crate::testing::TempEnvironment;
fn env_with_shell_pack() -> TempEnvironment {
TempEnvironment::builder()
.pack("vim")
.file("aliases.sh", "alias vi=vim")
.done()
.build()
}
fn simulate_activation(env: &TempEnvironment, generation: u64) {
simulate_activation_by(env, generation, activation::running_version());
}
fn simulate_activation_by(env: &TempEnvironment, generation: u64, version: &str) {
env.fs.mkdir_all(&env.paths.probes_hookup_dir()).unwrap();
env.fs
.write_file(
&env.paths.hookup_heartbeat_path(),
format!("{generation} {version}").as_bytes(),
)
.unwrap();
}
fn script_generation(env: &TempEnvironment) -> u64 {
activation::read_script_generation(env.fs.as_ref(), env.paths.as_ref()).unwrap()
}
fn live_stamp(generation: u64) -> activation::EnvStamp {
activation::EnvStamp {
generation: Some(generation),
version: Some(activation::running_version().into()),
}
}
#[test]
fn up_on_a_fresh_install_ends_with_the_never_activated_warning() {
let env = env_with_shell_pack();
let ctx = make_ctx(&env);
let result = up::up(None, &ctx).unwrap();
let notice = result
.shell_hookup
.expect("a green first up must not stay silent about the missing hookup");
assert_eq!(notice.state, "never-activated");
assert_eq!(notice.severity, "warning");
assert_eq!(notice.evidence, "Never loaded.");
let hint = notice.hint.expect("never-activated must say what to do");
assert!(
hint.contains("dodot-init.sh"),
"hint must name the manual hook line: {hint}"
);
}
#[test]
fn up_from_a_live_shell_reports_the_healthy_footer() {
let env = env_with_shell_pack();
up::up(None, &make_ctx(&env)).unwrap();
let generation = script_generation(&env);
simulate_activation(&env, generation);
let mut ctx = make_ctx(&env);
ctx.env_stamp = live_stamp(generation);
let result = up::up(None, &ctx).unwrap();
let notice = result.shell_hookup.expect("the footer is unconditional");
assert_eq!(notice.state, "healthy");
assert_eq!(
notice.message,
"Shell hookup: dodot is sourced in new shells."
);
assert_eq!(notice.hint, None);
}
#[test]
fn status_from_a_stale_shell_hints_at_opening_a_new_shell() {
let env = env_with_shell_pack();
up::up(None, &make_ctx(&env)).unwrap();
let generation = script_generation(&env);
simulate_activation(&env, generation - 1);
let mut ctx = make_ctx(&env);
ctx.env_stamp = live_stamp(generation - 1);
let notice = status::status(None, &ctx).unwrap().shell_hookup.unwrap();
assert_eq!(notice.state, "stale-shell");
assert_eq!(notice.severity, "info");
assert_eq!(
notice.message,
"Shell hookup: this shell predates your last `dodot up`."
);
assert!(notice.hint.unwrap().contains("Open a new shell"));
}
#[test]
fn status_reports_the_healthy_footer_for_a_healthy_hookup() {
let env = env_with_shell_pack();
up::up(None, &make_ctx(&env)).unwrap();
let generation = script_generation(&env);
simulate_activation(&env, generation);
let mut ctx = make_ctx(&env);
ctx.env_stamp = live_stamp(generation);
let notice = status::status(None, &ctx).unwrap().shell_hookup.unwrap();
assert_eq!(notice.state, "healthy");
assert_eq!(notice.severity, "ok");
assert_eq!(notice.hint, None);
assert_eq!(
notice.evidence,
format!(
"Last loaded just now by dodot {}.",
activation::running_version()
),
"line two dates the last activation from the heartbeat's mtime"
);
}
#[test]
fn status_reports_version_skew_naming_both_versions() {
let env = env_with_shell_pack();
up::up(None, &make_ctx(&env)).unwrap();
let generation = script_generation(&env);
simulate_activation_by(&env, generation, "5.0.0");
let notice = status::status(None, &make_ctx(&env))
.unwrap()
.shell_hookup
.unwrap();
assert_eq!(notice.state, "version-skew");
assert_eq!(
notice.severity, "warning",
"a user mid-upgrade sees this transiently — it is not an error"
);
assert_eq!(
notice.message,
"Shell hookup: your shells load a different dodot."
);
assert_eq!(
notice.evidence,
format!(
"Last loaded just now by dodot 5.0.0 — you are running {}.",
activation::running_version()
)
);
}
#[test]
fn status_reads_a_planted_pre_version_heartbeat_as_the_bound() {
let env = env_with_shell_pack();
up::up(None, &make_ctx(&env)).unwrap();
let generation = script_generation(&env);
env.fs.mkdir_all(&env.paths.probes_hookup_dir()).unwrap();
env.fs
.write_file(
&env.paths.hookup_heartbeat_path(),
generation.to_string().as_bytes(),
)
.unwrap();
let notice = status::status(None, &make_ctx(&env))
.unwrap()
.shell_hookup
.unwrap();
assert!(
notice.evidence.contains("dodot ≤5.5.1"),
"a version-less heartbeat is bounded, not unknown: {}",
notice.evidence
);
}
#[test]
fn status_stays_healthy_when_the_heartbeat_is_current_but_this_process_has_no_stamp() {
let env = env_with_shell_pack();
up::up(None, &make_ctx(&env)).unwrap();
let generation = script_generation(&env);
simulate_activation(&env, generation);
let notice = status::status(None, &make_ctx(&env))
.unwrap()
.shell_hookup
.unwrap();
assert_eq!(notice.state, "healthy");
}
#[test]
fn status_from_a_tty_shell_without_a_stamp_reports_the_dead_hookup() {
let env = env_with_shell_pack();
up::up(None, &make_ctx(&env)).unwrap();
let generation = script_generation(&env);
simulate_activation(&env, generation);
let mut ctx = make_ctx(&env);
ctx.tty = true;
ctx.shell_env = crate::shell::ShellEnv {
shell: Some("/bin/zsh".into()),
zdotdir: None,
};
let notice = status::status(None, &ctx).unwrap().shell_hookup.unwrap();
assert_eq!(notice.state, "shell-not-loaded");
assert_eq!(notice.severity, "warning");
assert_eq!(
notice.message, "Shell hookup: this shell did not load dodot.",
"the message states what is literally true"
);
let hint = notice.hint.unwrap();
assert!(
hint.contains("~/.zshrc") && hint.contains("dodot install --write"),
"an absent hook names the rc file and the fix: {hint}"
);
assert!(
!hint.contains("new shell"),
"no new shell fixes a missing hook: {hint}"
);
}
#[test]
fn status_after_an_up_does_not_advise_a_new_shell_when_the_hook_is_gone() {
let env = env_with_shell_pack();
up::up(None, &make_ctx(&env)).unwrap();
let generation = script_generation(&env);
simulate_activation(&env, generation - 1);
let mut ctx = make_ctx(&env);
ctx.tty = true;
ctx.shell_env = crate::shell::ShellEnv {
shell: Some("/bin/zsh".into()),
zdotdir: None,
};
let notice = status::status(None, &ctx).unwrap().shell_hookup.unwrap();
assert_eq!(notice.state, "shell-not-loaded");
let hint = notice.hint.unwrap();
assert!(
!hint.contains("new shell"),
"a missing hook must not get stale-shell advice: {hint}"
);
assert!(hint.contains("dodot install --write"), "hint: {hint}");
}
#[test]
fn status_from_a_tty_shell_with_the_hook_wired_advises_a_new_shell() {
let env = env_with_shell_pack();
up::up(None, &make_ctx(&env)).unwrap();
let generation = script_generation(&env);
simulate_activation(&env, generation);
let hook = activation::hook_line(&env.paths.init_script_path(), env.paths.home_dir());
env.fs
.write_file(&env.paths.home_dir().join(".zshrc"), hook.as_bytes())
.unwrap();
let mut ctx = make_ctx(&env);
ctx.tty = true;
ctx.shell_env = crate::shell::ShellEnv {
shell: Some("/bin/zsh".into()),
zdotdir: None,
};
let notice = status::status(None, &ctx).unwrap().shell_hookup.unwrap();
assert_eq!(notice.state, "shell-not-loaded");
assert_eq!(notice.severity, "info");
assert!(
notice.hint.unwrap().contains("new shell"),
"hook present: an old shell is the likely story"
);
}
#[test]
fn status_from_a_live_tty_shell_stays_healthy() {
let env = env_with_shell_pack();
up::up(None, &make_ctx(&env)).unwrap();
let generation = script_generation(&env);
simulate_activation(&env, generation);
let mut ctx = make_ctx(&env);
ctx.tty = true;
ctx.env_stamp = live_stamp(generation);
let notice = status::status(None, &ctx).unwrap().shell_hookup.unwrap();
assert_eq!(notice.state, "healthy");
}
#[test]
fn down_reports_the_wired_but_empty_script() {
let env = env_with_shell_pack();
up::up(None, &make_ctx(&env)).unwrap();
let generation = script_generation(&env);
simulate_activation(&env, generation);
let mut ctx = make_ctx(&env);
ctx.env_stamp = live_stamp(generation);
let result = crate::commands::down::down(None, &ctx).unwrap();
let notice = result
.shell_hookup
.expect("`down` carries the footer like every other render");
assert_eq!(notice.state, "empty-script");
assert_eq!(
notice.message,
"Shell hookup: wired, but no packs are deployed."
);
}
#[test]
fn an_all_ignored_repo_reports_the_wired_but_empty_script() {
let env = TempEnvironment::builder()
.pack("vim")
.file("aliases.sh", "alias vi=vim")
.file(".dodotignore", "")
.done()
.build();
up::up(None, &make_ctx(&env)).unwrap();
let generation = script_generation(&env);
simulate_activation(&env, generation);
let mut ctx = make_ctx(&env);
ctx.env_stamp = live_stamp(generation);
let notice = status::status(None, &ctx).unwrap().shell_hookup.unwrap();
assert_eq!(notice.state, "empty-script");
}
#[test]
fn the_footer_renders_both_lines() {
let env = env_with_shell_pack();
let result = up::up(None, &make_ctx(&env)).unwrap();
let text = render::render("pack-status", &result, OutputMode::Text).unwrap();
assert!(
text.contains("Shell hookup: no shell has loaded dodot yet."),
"line one must reach rendered output: {text}"
);
assert!(
text.contains("Never loaded."),
"line two must reach rendered output: {text}"
);
assert!(
text.contains("dodot install --write") && text.contains("dodot-init.sh"),
"the fix — the command and the manual line — must reach rendered output: {text}"
);
let tagged = render::render("pack-status", &result, OutputMode::TermDebug).unwrap();
assert!(
tagged.contains("[warning]⚠ Shell hookup: no shell has loaded dodot yet.[/warning]"),
"never-activated renders in the warning style: {tagged}"
);
assert!(
tagged.contains("[dim]Never loaded.[/dim]"),
"the evidence line renders dimmed: {tagged}"
);
}
#[test]
fn a_healthy_footer_renders_in_the_ok_style() {
let env = env_with_shell_pack();
up::up(None, &make_ctx(&env)).unwrap();
let generation = script_generation(&env);
simulate_activation(&env, generation);
let mut ctx = make_ctx(&env);
ctx.env_stamp = live_stamp(generation);
let result = status::status(None, &ctx).unwrap();
let tagged = render::render("pack-status", &result, OutputMode::TermDebug).unwrap();
assert!(
tagged.contains("[deployed]✓ Shell hookup: dodot is sourced in new shells.[/deployed]"),
"a working hookup reads as working: {tagged}"
);
assert!(
tagged.contains("[dim]Last loaded just now by dodot"),
"the evidence line renders dimmed: {tagged}"
);
}