aim/crates/aim-core/tests/install_integration.rs
stoorps 9d8ec1e4fd
feat(cli): enhance install and removal UX with progress visibility and theming
- Introduced visible progress stages during installation, including source resolution and artifact selection.
- Improved separation between live transcript output and final summaries, ensuring clarity.
- Removed redundant recap text from installation summaries.
- Centralized terminal styling using a configurable theme system, allowing for warm defaults and user overrides.
- Added support for hex colors and named colors in the configuration.
- Updated tests to verify new behaviors and configurations.
2026-03-20 19:44:04 +00:00

228 lines
7.9 KiB
Rust

use aim_core::app::add::{build_add_plan_with_reporter, install_app_with_reporter};
use aim_core::app::progress::{OperationEvent, OperationStage};
use aim_core::domain::app::InstallScope;
use aim_core::integration::install::{DesktopIntegrationRequest, InstallRequest, execute_install};
use aim_core::platform::DesktopHelpers;
use aim_core::source::github::FixtureGitHubTransport;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use tempfile::tempdir;
#[test]
fn install_writes_desktop_entry_and_reports_refresh_warning_only() {
let root = tempdir().unwrap();
let staging_root = root.path().join("staging");
let payload_root = root.path().join("payloads");
let desktop_root = root.path().join("applications");
fs::create_dir(&staging_root).unwrap();
fs::create_dir(&payload_root).unwrap();
fs::create_dir(&desktop_root).unwrap();
let outcome = execute_install(&InstallRequest {
staging_root: &staging_root,
final_payload_path: &payload_root.join("bat.AppImage"),
artifact_bytes: b"\x7fELFAppImage",
desktop: Some(DesktopIntegrationRequest {
desktop_entry_path: &desktop_root.join("aim-bat.desktop"),
desktop_entry_contents: "[Desktop Entry]\nName=bat\nExec=bat.AppImage\nType=Application\n",
icon_path: None,
icon_bytes: None,
}),
helpers: DesktopHelpers::default(),
})
.unwrap();
assert!(outcome.desktop_entry_path.unwrap().exists());
assert!(!outcome.warnings.is_empty());
}
#[test]
fn install_executes_refresh_helpers_when_available() {
let root = tempdir().unwrap();
let staging_root = root.path().join("staging");
let payload_root = root.path().join("payloads");
let desktop_root = root.path().join("applications");
let helper_root = root.path().join("helpers");
let log_path = root.path().join("helpers.log");
fs::create_dir(&staging_root).unwrap();
fs::create_dir(&payload_root).unwrap();
fs::create_dir(&desktop_root).unwrap();
fs::create_dir(&helper_root).unwrap();
let update_helper = helper_root.join("update-desktop-database");
let icon_helper = helper_root.join("gtk-update-icon-cache");
fs::write(
&update_helper,
format!("#!/bin/sh\necho desktop:$1 >> {}\n", log_path.display()),
)
.unwrap();
fs::write(
&icon_helper,
format!("#!/bin/sh\necho icon:$3 >> {}\n", log_path.display()),
)
.unwrap();
fs::set_permissions(&update_helper, fs::Permissions::from_mode(0o755)).unwrap();
fs::set_permissions(&icon_helper, fs::Permissions::from_mode(0o755)).unwrap();
let icon_root = root.path().join("icons/hicolor/256x256/apps");
fs::create_dir_all(&icon_root).unwrap();
let outcome = execute_install(&InstallRequest {
staging_root: &staging_root,
final_payload_path: &payload_root.join("bat.AppImage"),
artifact_bytes: b"\x7fELFAppImage\x89PNG\r\n\x1a\nicondataIEND\xaeB`\x82",
desktop: Some(DesktopIntegrationRequest {
desktop_entry_path: &desktop_root.join("aim-bat.desktop"),
desktop_entry_contents: "[Desktop Entry]\nName=bat\nExec=bat.AppImage\nType=Application\n",
icon_path: Some(&icon_root.join("bat.png")),
icon_bytes: None,
}),
helpers: DesktopHelpers {
update_desktop_database: true,
gtk_update_icon_cache: true,
update_desktop_database_path: Some(update_helper),
gtk_update_icon_cache_path: Some(icon_helper),
},
})
.unwrap();
assert!(outcome.warnings.is_empty());
let log = fs::read_to_string(&log_path).unwrap();
assert!(log.contains("desktop:"));
assert!(log.contains("icon:"));
}
#[test]
fn install_extracts_icon_from_appimage_payload_when_icon_path_is_requested() {
let root = tempdir().unwrap();
let staging_root = root.path().join("staging");
let payload_root = root.path().join("payloads");
let desktop_root = root.path().join("applications");
let icon_root = root.path().join("icons/hicolor/256x256/apps");
fs::create_dir(&staging_root).unwrap();
fs::create_dir(&payload_root).unwrap();
fs::create_dir(&desktop_root).unwrap();
fs::create_dir_all(&icon_root).unwrap();
let outcome = execute_install(&InstallRequest {
staging_root: &staging_root,
final_payload_path: &payload_root.join("bat.AppImage"),
artifact_bytes: b"\x7fELFAppImage\x89PNG\r\n\x1a\nicondataIEND\xaeB`\x82",
desktop: Some(DesktopIntegrationRequest {
desktop_entry_path: &desktop_root.join("aim-bat.desktop"),
desktop_entry_contents: "[Desktop Entry]\nName=bat\nExec=bat.AppImage\nType=Application\n",
icon_path: Some(&icon_root.join("bat.png")),
icon_bytes: None,
}),
helpers: DesktopHelpers::default(),
})
.unwrap();
let icon_path = outcome.icon_path.unwrap();
assert!(icon_path.exists());
assert!(
fs::read(&icon_path)
.unwrap()
.starts_with(b"\x89PNG\r\n\x1a\n")
);
}
#[test]
fn install_app_reports_operation_stages_in_order() {
let root = tempdir().unwrap();
let mut events: Vec<OperationEvent> = Vec::new();
unsafe {
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
}
let mut reporter = |event: &OperationEvent| events.push(event.clone());
let plan = build_add_plan_with_reporter("sharkdp/bat", &FixtureGitHubTransport, &mut reporter)
.unwrap();
let installed = install_app_with_reporter(
"sharkdp/bat",
&plan,
root.path(),
InstallScope::User,
&mut reporter,
)
.unwrap();
assert_eq!(installed.record.stable_id, "sharkdp-bat");
assert!(events.contains(&OperationEvent::StageChanged {
stage: OperationStage::ResolveQuery,
message: "resolving source".to_owned(),
}));
assert!(events.contains(&OperationEvent::StageChanged {
stage: OperationStage::DiscoverRelease,
message: "discovering release".to_owned(),
}));
assert!(events.contains(&OperationEvent::StageChanged {
stage: OperationStage::SelectArtifact,
message: "selecting artifact".to_owned(),
}));
assert!(events.contains(&OperationEvent::StageChanged {
stage: OperationStage::DownloadArtifact,
message: "downloading artifact".to_owned(),
}));
assert!(events.contains(&OperationEvent::StageChanged {
stage: OperationStage::StagePayload,
message: "staging payload".to_owned(),
}));
assert!(events.iter().any(|event| {
matches!(
event,
OperationEvent::Progress {
current,
total: Some(total)
} if *current == *total
)
}));
assert!(events.contains(&OperationEvent::StageChanged {
stage: OperationStage::WriteDesktopEntry,
message: "writing desktop entry".to_owned(),
}));
assert!(events.iter().any(|event| {
matches!(
event,
OperationEvent::StageChanged {
stage: OperationStage::RefreshIntegration,
..
}
)
}));
let stage_order = events
.iter()
.filter_map(|event| match event {
OperationEvent::StageChanged { stage, .. } => Some(*stage),
_ => None,
})
.collect::<Vec<_>>();
assert!(stage_order.windows(2).any(|window| {
window
== [
OperationStage::ResolveQuery,
OperationStage::DiscoverRelease,
]
}));
assert!(stage_order.windows(2).any(|window| {
window
== [
OperationStage::DiscoverRelease,
OperationStage::SelectArtifact,
]
}));
assert!(stage_order.windows(2).any(|window| {
window
== [
OperationStage::SelectArtifact,
OperationStage::DownloadArtifact,
]
}));
}