Implement update execution and provider contract

This commit is contained in:
stoorps 2026-03-20 00:15:40 +00:00
parent 842c390260
commit d8eb7b3cab
Signed by: stoorps
SSH key fingerprint: SHA256:AZlPfu9hTu042EGtZElmDQoy+KvMOeShLDan/fYLoNI
16 changed files with 407 additions and 79 deletions

View file

@ -18,8 +18,7 @@ pub struct Cli {
impl Cli {
pub fn is_review_update_flow(&self) -> bool {
matches!(self.command, Some(Command::Update))
|| (self.command.is_none() && self.query.is_none())
self.command.is_none() && self.query.is_none()
}
}

View file

@ -9,9 +9,9 @@ use aim_core::app::add::{
};
use aim_core::app::list::{ListRow, build_list_rows};
use aim_core::app::remove::{RemovalResult, remove_registered_app};
use aim_core::app::update::build_update_plan;
use aim_core::app::update::{build_update_plan, execute_updates};
use aim_core::domain::app::AppRecord;
use aim_core::domain::update::UpdatePlan;
use aim_core::domain::update::{UpdateExecutionResult, UpdatePlan};
use aim_core::registry::model::Registry;
use aim_core::registry::store::RegistryStore;
@ -44,7 +44,15 @@ pub fn dispatch(cli: Cli) -> Result<DispatchResult, DispatchError> {
})?;
Ok(DispatchResult::Removed(Box::new(removal)))
}
cli::args::Command::Update => Ok(DispatchResult::UpdatePlan(build_update_plan(&apps)?)),
cli::args::Command::Update => {
let updates = execute_updates(&apps, &install_home)?;
let updated_apps = updates.apps.clone();
store.save(&Registry {
version: registry.version,
apps: updated_apps,
})?;
Ok(DispatchResult::Updated(Box::new(updates)))
}
};
}
@ -94,6 +102,7 @@ pub enum DispatchResult {
PendingAdd(Box<AddPlan>),
Removed(Box<RemovalResult>),
UpdatePlan(UpdatePlan),
Updated(Box<UpdateExecutionResult>),
Noop,
}
@ -105,6 +114,7 @@ pub enum DispatchError {
RemovePlan(aim_core::app::remove::RemoveRegisteredAppError),
Registry(aim_core::registry::store::RegistryStoreError),
UpdatePlan(aim_core::app::update::BuildUpdatePlanError),
UpdateExecution(aim_core::app::update::ExecuteUpdatesError),
}
impl From<aim_core::app::add::BuildAddPlanError> for DispatchError {
@ -131,6 +141,12 @@ impl From<aim_core::app::update::BuildUpdatePlanError> for DispatchError {
}
}
impl From<aim_core::app::update::ExecuteUpdatesError> for DispatchError {
fn from(value: aim_core::app::update::ExecuteUpdatesError) -> Self {
Self::UpdateExecution(value)
}
}
impl From<aim_core::app::remove::RemoveRegisteredAppError> for DispatchError {
fn from(value: aim_core::app::remove::RemoveRegisteredAppError) -> Self {
Self::RemovePlan(value)

View file

@ -1,4 +1,5 @@
use aim_core::app::add::AddPlan;
use aim_core::domain::update::UpdateExecutionStatus;
use crate::DispatchResult;
@ -15,6 +16,7 @@ pub fn render_dispatch_result(result: &DispatchResult) -> String {
DispatchResult::UpdatePlan(plan) => {
render_update_summary(plan.items.len(), plan.items.len(), 0)
}
DispatchResult::Updated(result) => render_updated_apps(result),
DispatchResult::Noop => String::new(),
}
}
@ -88,3 +90,29 @@ fn render_removed_app(removed: &aim_core::app::remove::RemovalResult) -> String
format!("{summary}\n{warning_lines}")
}
}
fn render_updated_apps(result: &aim_core::domain::update::UpdateExecutionResult) -> String {
let mut lines = vec![format!(
"updated apps: {}, failed: {}",
result.updated_count(),
result.failed_count()
)];
for item in &result.items {
match &item.status {
UpdateExecutionStatus::Updated => lines.push(format!(
"updated: {} ({}) {} -> {}",
item.display_name,
item.stable_id,
item.from_version.as_deref().unwrap_or("unknown"),
item.to_version.as_deref().unwrap_or("unknown")
)),
UpdateExecutionStatus::Failed { reason } => lines.push(format!(
"failed: {} ({}) {}",
item.display_name, item.stable_id, reason
)),
}
}
lines.join("\n")
}

View file

@ -1,4 +1,8 @@
use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
use aim_core::registry::model::Registry;
use aim_core::registry::store::RegistryStore;
use assert_cmd::Command;
use predicates::prelude::PredicateBooleanExt;
use predicates::str::contains;
use tempfile::tempdir;
@ -179,3 +183,49 @@ fn system_request_on_immutable_host_falls_back_to_user_install() {
.stdout(contains("installing as user"))
.stdout(contains("downgraded to user scope"));
}
#[test]
fn update_command_applies_updates() {
let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml");
let payload_path = dir
.path()
.join("install-home/.local/lib/aim/appimages/pingdotgg-t3code.AppImage");
let store = RegistryStore::new(registry_path.clone());
store
.save(&Registry {
version: 1,
apps: vec![AppRecord {
stable_id: "pingdotgg-t3code".to_owned(),
display_name: "t3code".to_owned(),
source_input: Some("pingdotgg/t3code".to_owned()),
source: None,
installed_version: Some("0.0.11".to_owned()),
update_strategy: None,
metadata: Vec::new(),
install: Some(InstallMetadata {
scope: InstallScope::User,
payload_path: None,
desktop_entry_path: None,
icon_path: None,
}),
}],
})
.unwrap();
let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("update")
.env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1")
.assert()
.success()
.stdout(contains("updated apps: 1"))
.stdout(contains("updates found:").not());
let updated = store.load().unwrap();
assert_eq!(updated.apps.len(), 1);
assert_eq!(updated.apps[0].stable_id, "pingdotgg-t3code");
assert_eq!(updated.apps[0].installed_version.as_deref(), Some("0.0.12"));
assert!(payload_path.exists());
}