use super::*;
#[test]
fn rc_path_guard_is_idempotent() {
let bin = "/home/jb/.astrid/bin";
let export = format!("export PATH=\"{bin}:$PATH\"");
assert!(!rc_configures_path("", bin, &export));
let after_first_write = format!("# existing\n\n# Astrid OS\n{export}\n");
assert!(rc_configures_path(&after_first_write, bin, &export));
let manual = format!("export PATH=$PATH:{bin}\n");
assert!(rc_configures_path(&manual, bin, &export));
assert!(!rc_configures_path(
"export PATH=\"/usr/bin:$PATH\"\n",
bin,
&export
));
}
#[test]
fn rc_path_guard_rejects_substring_false_positives() {
let bin = "/home/jb/.astrid/bin";
let export = format!("export PATH=\"{bin}:$PATH\"");
let backup = "export PATH=\"/home/jb/.astrid/bin_backup:$PATH\"\n";
assert!(!rc_configures_path(backup, bin, &export));
let subdir = "export PATH=\"/home/jb/.astrid/bin/foo:$PATH\"\n";
assert!(!rc_configures_path(subdir, bin, &export));
let bounded = "export PATH=\"/home/jb/.astrid/bin:$PATH\"\n";
assert!(rc_configures_path(bounded, bin, &export));
let mixed = "PATH=/home/jb/.astrid/bin_backup\nPATH=/home/jb/.astrid/bin\n";
assert!(rc_configures_path(mixed, bin, &export));
}
#[test]
fn rc_path_guard_ignores_commented_lines() {
let bin = "/home/jb/.astrid/bin";
let export = format!("export PATH=\"{bin}:$PATH\"");
let commented = "# PATH=/home/jb/.astrid/bin\n";
assert!(!rc_configures_path(commented, bin, &export));
let inline = "echo hi # note: /home/jb/.astrid/bin\n";
assert!(!rc_configures_path(inline, bin, &export));
let active = "PATH=/home/jb/.astrid/bin\n";
assert!(rc_configures_path(active, bin, &export));
let both = "# PATH=/home/jb/.astrid/bin\nPATH=/home/jb/.astrid/bin\n";
assert!(rc_configures_path(both, bin, &export));
}
#[test]
fn rc_path_guard_ignores_commented_exact_block() {
let bin = "/home/jb/.astrid/bin";
let export = format!("export PATH=\"{bin}:$PATH\"");
let commented_exact = format!("# {export}\n");
assert!(!rc_configures_path(&commented_exact, bin, &export));
let active_exact = format!("{export}\n");
assert!(rc_configures_path(&active_exact, bin, &export));
let both_exact = format!("# {export}\n{export}\n");
assert!(rc_configures_path(&both_exact, bin, &export));
}
#[test]
fn homebrew_path_is_detected() {
assert!(is_homebrew_managed(Path::new(
"/opt/homebrew/Cellar/astrid/0.8.0/bin/astrid"
)));
assert!(is_homebrew_managed(Path::new(
"/usr/local/Cellar/astrid/0.8.0/bin/astrid"
)));
assert!(!is_homebrew_managed(Path::new(
"/Users/jb/.astrid/bin/astrid"
)));
assert!(!is_homebrew_managed(Path::new("/usr/local/bin/astrid")));
assert!(!is_homebrew_managed(Path::new(
"/home/jb/.cargo/bin/astrid"
)));
}
#[test]
fn install_method_is_detected_per_path() {
use InstallMethod::{Cargo, Homebrew, SelfManaged};
assert_eq!(
InstallMethod::detect(Path::new("/opt/homebrew/Cellar/astrid/0.9.2/bin/astrid")),
Homebrew
);
assert_eq!(
InstallMethod::detect(Path::new("/home/jb/.cargo/bin/astrid")),
Cargo
);
assert_eq!(
InstallMethod::detect(Path::new("/Users/jb/.astrid/bin/astrid")),
SelfManaged
);
assert_eq!(
InstallMethod::detect(Path::new("/usr/local/bin/astrid")),
SelfManaged
);
assert_eq!(
InstallMethod::detect(Path::new("/home/jb/.cargo/registry/astrid")),
SelfManaged
);
}
#[test]
fn check_reports_update_for_all_install_methods() {
use InstallMethod::{Cargo, Homebrew, SelfManaged};
let older = semver::Version::parse("0.9.1").unwrap();
let newer = semver::Version::parse("0.9.2").unwrap();
for method in [Homebrew, Cargo, SelfManaged] {
assert_eq!(
plan_update(method, &older, &newer, true),
UpdatePlan::Available {
how: method.upgrade_command()
},
"check must report availability for {method:?}"
);
assert_eq!(
plan_update(method, &newer, &newer, true),
UpdatePlan::UpToDate
);
assert_eq!(
plan_update(method, &newer, &older, false),
UpdatePlan::UpToDate
);
}
assert_eq!(
plan_update(Homebrew, &older, &newer, false),
UpdatePlan::DeferToManager {
manager: "Homebrew",
how: "brew upgrade astrid"
}
);
assert_eq!(
plan_update(Cargo, &older, &newer, false),
UpdatePlan::DeferToManager {
manager: "cargo",
how: "cargo install astrid --force"
}
);
assert_eq!(
plan_update(SelfManaged, &older, &newer, false),
UpdatePlan::ApplyInPlace
);
}
#[test]
fn resolve_repo_precedence_and_validation() {
assert_eq!(
resolve_repo(Some("acme/astrid")).unwrap(),
("acme".to_string(), "astrid".to_string())
);
assert!(resolve_repo(Some("no-slash")).is_err());
assert!(resolve_repo(Some("owner/")).is_err());
assert!(resolve_repo(Some("/repo")).is_err());
}
#[test]
fn sha256_verification_matches_and_rejects() {
use sha2::Digest;
let archive = b"hello astrid";
let good = to_hex(&sha2::Sha256::digest(archive));
let body = format!("{good} astrid-1.0.0-x.tar.gz\n");
verify_sha256(archive, &body, "astrid-1.0.0-x.tar.gz").expect("matching sum verifies");
let bad_body = format!("{} astrid-1.0.0-x.tar.gz\n", "0".repeat(64));
assert!(verify_sha256(archive, &bad_body, "astrid-1.0.0-x.tar.gz").is_err());
assert!(verify_sha256(archive, "deadbeef other.tar.gz\n", "astrid-1.0.0-x.tar.gz").is_err());
}
#[test]
fn backup_and_swap_replaces_and_keeps_backup() {
let dir = tempfile::tempdir().unwrap();
let install = dir.path().join("bin");
let extract = dir.path().join("new");
std::fs::create_dir_all(&install).unwrap();
std::fs::create_dir_all(&extract).unwrap();
std::fs::write(install.join("astrid"), b"OLD").unwrap();
std::fs::write(install.join("astrid-daemon"), b"OLD-D").unwrap();
std::fs::write(extract.join("astrid"), b"NEW").unwrap();
std::fs::write(extract.join("astrid-daemon"), b"NEW-D").unwrap();
backup_and_swap(&install, &extract, MANAGED_BINARIES).unwrap();
assert_eq!(std::fs::read(install.join("astrid")).unwrap(), b"NEW");
assert_eq!(
std::fs::read(install.join("astrid-daemon")).unwrap(),
b"NEW-D"
);
assert_eq!(std::fs::read(install.join("astrid.bak")).unwrap(), b"OLD");
assert_eq!(
std::fs::read(install.join("astrid-daemon.bak")).unwrap(),
b"OLD-D"
);
assert!(!install.join(".astrid.new").exists());
}
#[test]
fn backup_and_swap_bails_when_archive_missing_a_binary() {
let dir = tempfile::tempdir().unwrap();
let install = dir.path().join("bin");
let extract = dir.path().join("new");
std::fs::create_dir_all(&install).unwrap();
std::fs::create_dir_all(&extract).unwrap();
std::fs::write(install.join("astrid"), b"OLD").unwrap();
std::fs::write(install.join("astrid-daemon"), b"OLD-D").unwrap();
std::fs::write(extract.join("astrid"), b"NEW").unwrap();
assert!(backup_and_swap(&install, &extract, MANAGED_BINARIES).is_err());
assert_eq!(std::fs::read(install.join("astrid")).unwrap(), b"OLD");
assert_eq!(
std::fs::read(install.join("astrid-daemon")).unwrap(),
b"OLD-D"
);
assert!(!install.join("astrid.bak").exists());
assert!(!install.join(".astrid.new").exists());
}
#[test]
fn post_update_sync_message_softens_version_gate() {
use super::super::distro::validate::AstridVersionTooOld;
use anyhow::Context as _;
let gate: anyhow::Error = AstridVersionTooOld {
req: ">=0.8.0".to_string(),
running: "0.7.0".to_string(),
}
.into();
let msg = post_update_sync_message(&gate);
assert!(
msg.contains("take effect")
&& msg.contains("next run")
&& !msg.contains("Run `astrid update`"),
"version-gate failure must yield the benign restart message, got: {msg}"
);
let wrapped: anyhow::Error = Err::<(), _>(anyhow::Error::from(AstridVersionTooOld {
req: ">=0.8.0".to_string(),
running: "0.7.0".to_string(),
}))
.context("re-running init after update")
.context("syncing distro")
.unwrap_err();
assert_eq!(wrapped.to_string(), "syncing distro");
assert!(
wrapped
.chain()
.any(<dyn std::error::Error + 'static>::is::<AstridVersionTooOld>),
"guard: the typed gate must be reachable by walking the chain"
);
let msg = post_update_sync_message(&wrapped);
assert!(
msg.contains("take effect")
&& msg.contains("next run")
&& !msg.contains("Run `astrid update`"),
"context-wrapped version-gate failure must still be softened, got: {msg}"
);
let other = anyhow::anyhow!("network unreachable while fetching Distro.toml");
let msg = post_update_sync_message(&other);
assert!(
msg.starts_with("Distro sync:") && msg.contains("network unreachable"),
"non-gate failure must use the generic warn path, got: {msg}"
);
}