use super::*;
#[test]
fn installed_distro_lock_selects_skip_without_a_remote_source() {
assert_eq!(
distro_refresh_action(true),
DistroRefreshAction::SkipNoProvenance,
"a lock records an identity, not a source, so refresh must not invoke init"
);
assert_eq!(
distro_refresh_action(false),
DistroRefreshAction::NoInstalledDistro
);
}
#[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, UpdateChannel::Stable),
UpdatePlan::Available {
how: method.upgrade_command(UpdateChannel::Stable)
},
"check must report availability for {method:?}"
);
assert_eq!(
plan_update(method, &newer, &newer, true, UpdateChannel::Stable),
UpdatePlan::UpToDate
);
}
assert_eq!(
plan_update(Homebrew, &older, &newer, false, UpdateChannel::Stable),
UpdatePlan::DeferToManager {
manager: "Homebrew",
how: "brew upgrade astrid"
}
);
assert_eq!(
plan_update(Cargo, &older, &newer, false, UpdateChannel::Stable),
UpdatePlan::DeferToManager {
manager: "cargo",
how: "cargo install astrid --force"
}
);
assert_eq!(
plan_update(SelfManaged, &older, &newer, false, UpdateChannel::Stable),
UpdatePlan::ApplyInPlace
);
assert_eq!(
plan_update(SelfManaged, &newer, &older, false, UpdateChannel::Stable),
UpdatePlan::ApplyInPlace
);
}
#[test]
fn package_manager_commands_follow_the_signed_stable_version() {
let older = semver::Version::parse("1.2.2").unwrap();
let selected = semver::Version::parse("1.2.3").unwrap();
assert_eq!(
notice::managed_update_command(InstallMethod::Cargo, &older, &selected, "1.2.3").unwrap(),
"cargo install astrid --version =1.2.3 --force"
);
assert_eq!(
notice::managed_update_command(InstallMethod::Homebrew, &older, &selected, "1.2.3")
.unwrap(),
"brew upgrade astrid-runtime/tap/astrid"
);
assert_eq!(
notice::managed_update_command(InstallMethod::Homebrew, &selected, &older, "1.2.2")
.unwrap(),
"brew reinstall astrid-runtime/tap/astrid"
);
assert_eq!(
notice::managed_update_command(InstallMethod::Cargo, &selected, &older, "1.2.2").unwrap(),
"cargo install astrid --version =1.2.2 --force"
);
assert!(
notice::managed_update_command(InstallMethod::SelfManaged, &older, &selected, "1.2.3")
.is_none()
);
}
#[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 release_asset_lookup_requires_one_exact_asset() {
let release = serde_json::json!({
"assets": [
{
"name": "astrid-1.0.0-x.tar.gz",
"browser_download_url": "https://example.com/archive"
},
{
"name": "astrid-1.0.0-x.tar.gz.sigstore.json",
"browser_download_url": "https://example.com/bundle"
},
{
"name": "BLAKE3SUMS.txt",
"browser_download_url": "https://example.com/sums"
}
]
});
assert_eq!(
exact_asset_url(&release, "astrid-1.0.0-x.tar.gz").unwrap(),
"https://example.com/archive"
);
assert!(exact_asset_url(&release, "astrid-1.0.0-y.tar.gz").is_err());
let duplicate = serde_json::json!({
"assets": [
{
"name": "BLAKE3SUMS.txt",
"browser_download_url": "https://example.com/one"
},
{
"name": "BLAKE3SUMS.txt",
"browser_download_url": "https://example.com/two"
}
]
});
assert!(exact_asset_url(&duplicate, "BLAKE3SUMS.txt").is_err());
let oversized = serde_json::json!({
"assets": vec![serde_json::json!({"name": "irrelevant"}); MAX_RELEASE_ASSETS + 1]
});
assert!(
exact_asset_url(&oversized, "irrelevant")
.unwrap_err()
.to_string()
.contains("too many assets")
);
}
#[test]
fn publisher_bundle_and_blake3_manifest_are_both_mandatory() {
let sha_only = serde_json::json!({
"assets": [{
"name": "SHA256SUMS.txt",
"browser_download_url": "https://example.com/SHA256SUMS.txt"
}]
});
assert!(matches!(
integrity_manifest_url(&sha_only).unwrap_err(),
UpdateStageError::Integrity(_)
));
assert!(matches!(
publisher_bundle_url(&sha_only, "astrid-1.0.0-x.tar.gz").unwrap_err(),
UpdateStageError::PublisherAuthentication(_)
));
}
#[test]
fn staged_asset_selection_preserves_the_exact_failure() {
let bundle_name = "astrid-1.0.0-x.tar.gz.sigstore.json";
let duplicate_bundle = serde_json::json!({
"assets": [
{"name": bundle_name, "browser_download_url": "https://example.com/one"},
{"name": bundle_name, "browser_download_url": "https://example.com/two"}
]
});
let error = publisher_bundle_url(&duplicate_bundle, "astrid-1.0.0-x.tar.gz")
.unwrap_err()
.to_string();
assert_eq!(
error,
format!(
"publisher authentication failed: release contains duplicate asset '{bundle_name}'"
)
);
let oversized = serde_json::json!({
"assets": vec![serde_json::json!({"name": "irrelevant"}); MAX_RELEASE_ASSETS + 1]
});
assert_eq!(
integrity_manifest_url(&oversized).unwrap_err().to_string(),
"integrity check failed: release contains too many assets"
);
}
#[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());
}