From d8a080fe2daf61da381828929bd5d8760edf4efc Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 15:06:21 +0000 Subject: [PATCH] feat(dfx-orbit,wallet): support --wasm-memory-persistence for external canister upgrades Expose the wasm_memory_persistence upgrade option (added to the station API in #634) on the two client surfaces that create ChangeExternalCanister requests. dfx-orbit CLI: - `request canister install --mode upgrade` gains `--wasm-memory-persistence ` and `--skip-pre-upgrade`. Both are only valid with `--mode upgrade` and are rejected otherwise. - When neither flag is set the request is built as Upgrade(None) so it matches requests created without the feature; `verify` now compares the full upgrade options and the change operation display shows them. - Unit tests cover mode building/validation, plus an integration test that asserts the `keep` option round-trips through the station. Wallet Install screen: - The upgrade mode now shows a "Wasm memory persistence" select (keep/replace, clearable) and a "Skip pre-upgrade hook" checkbox, wired into the CanisterInstallMode.upgrade options. - The options collapse back to `{ upgrade: [] }` when unset, matching the CLI, and the mode select stays selected once options are attached. - Locale strings added for en/fr/pt; component and form tests added. `keep` is required for Motoko canisters that use Enhanced Orthogonal Persistence; without it the IC clears their main memory on upgrade. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TB34uukEX3a8qNTZi5HpH1 --- .../CanisterInstallForm.spec.ts | 62 +++++ .../CanisterInstallForm.vue | 64 ++++- .../inputs/CanisterInstallModeSelect.vue | 9 +- .../inputs/WasmMemoryPersistenceSelect.vue | 64 +++++ apps/wallet/src/locales/en.locale.ts | 7 + apps/wallet/src/locales/fr.locale.ts | 7 + apps/wallet/src/locales/pt.locale.ts | 7 + apps/wallet/src/types/station.types.ts | 8 + tests/integration/src/dfx_orbit/install.rs | 104 +++++++- tools/dfx-orbit/README.md | 13 + tools/dfx-orbit/src/canister.rs | 3 +- tools/dfx-orbit/src/canister/install.rs | 245 +++++++++++++++++- 12 files changed, 583 insertions(+), 10 deletions(-) create mode 100644 apps/wallet/src/components/inputs/WasmMemoryPersistenceSelect.vue diff --git a/apps/wallet/src/components/external-canisters/CanisterInstallForm.spec.ts b/apps/wallet/src/components/external-canisters/CanisterInstallForm.spec.ts index a622d17d0..943a2020e 100644 --- a/apps/wallet/src/components/external-canisters/CanisterInstallForm.spec.ts +++ b/apps/wallet/src/components/external-canisters/CanisterInstallForm.spec.ts @@ -1,7 +1,9 @@ import { Principal } from '@dfinity/principal'; import { describe, expect, it } from 'vitest'; +import WasmMemoryPersistenceSelect from '~/components/inputs/WasmMemoryPersistenceSelect.vue'; import { mount } from '~/test.utils'; import CanisterInstallForm from './CanisterInstallForm.vue'; +import { CanisterInstallModel } from './external-canisters.types'; describe('CanisterInstallForm', () => { it('hides the canisterId when display is set to false', () => { @@ -27,4 +29,64 @@ describe('CanisterInstallForm', () => { expect(canisterIdInput.exists()).toBe(true); }); + + it('shows the upgrade options only for the upgrade mode', () => { + const upgradeForm = mount(CanisterInstallForm, { + props: { + modelValue: { canisterId: Principal.anonymous(), mode: { upgrade: [] } }, + }, + }); + expect(upgradeForm.find('[name="wasm_memory_persistence"]').exists()).toBe(true); + expect(upgradeForm.find('[name="skip_pre_upgrade"]').exists()).toBe(true); + + const installForm = mount(CanisterInstallForm, { + props: { + modelValue: { canisterId: Principal.anonymous(), mode: { install: null } }, + }, + }); + expect(installForm.find('[name="wasm_memory_persistence"]').exists()).toBe(false); + expect(installForm.find('[name="skip_pre_upgrade"]').exists()).toBe(false); + }); + + it('writes the selected wasm memory persistence into the upgrade mode', async () => { + const form = mount(CanisterInstallForm, { + props: { + modelValue: { canisterId: Principal.anonymous(), mode: { upgrade: [] } }, + }, + }); + + const select = form.findComponent(WasmMemoryPersistenceSelect); + expect(select.exists()).toBe(true); + + select.vm.$emit('update:modelValue', { keep: null }); + await form.vm.$nextTick(); + + const emitted = form.emitted('update:modelValue') as CanisterInstallModel[][] | undefined; + expect(emitted).toBeTruthy(); + const latest = emitted![emitted!.length - 1][0]; + expect(latest.mode).toEqual({ + upgrade: [{ wasm_memory_persistence: [{ keep: null }], skip_pre_upgrade: [] }], + }); + }); + + it('collapses the upgrade options back to an empty upgrade when cleared', async () => { + const form = mount(CanisterInstallForm, { + props: { + modelValue: { + canisterId: Principal.anonymous(), + mode: { upgrade: [{ wasm_memory_persistence: [{ keep: null }], skip_pre_upgrade: [] }] }, + }, + }, + }); + + const select = form.findComponent(WasmMemoryPersistenceSelect); + // Vuetify's `clearable` emits `undefined`/`null`. + select.vm.$emit('update:modelValue', undefined); + await form.vm.$nextTick(); + + const emitted = form.emitted('update:modelValue') as CanisterInstallModel[][] | undefined; + expect(emitted).toBeTruthy(); + const latest = emitted![emitted!.length - 1][0]; + expect(latest.mode).toEqual({ upgrade: [] }); + }); }); diff --git a/apps/wallet/src/components/external-canisters/CanisterInstallForm.vue b/apps/wallet/src/components/external-canisters/CanisterInstallForm.vue index adb827375..1e0defd7d 100644 --- a/apps/wallet/src/components/external-canisters/CanisterInstallForm.vue +++ b/apps/wallet/src/components/external-canisters/CanisterInstallForm.vue @@ -15,6 +15,25 @@ + diff --git a/apps/wallet/src/locales/en.locale.ts b/apps/wallet/src/locales/en.locale.ts index c9ef374c3..d6cb52e1c 100644 --- a/apps/wallet/src/locales/en.locale.ts +++ b/apps/wallet/src/locales/en.locale.ts @@ -663,6 +663,13 @@ export default { upgrade: 'Upgrade', install: 'Install', }, + wasm_memory_persistence: { + label: 'Wasm memory persistence', + keep: 'Keep', + replace: 'Replace', + hint: 'Use "Keep" to preserve the canister main memory on upgrade, as required by Motoko canisters that use Enhanced Orthogonal Persistence.', + }, + skip_pre_upgrade: 'Skip pre-upgrade hook', }, terms: { license: 'License', diff --git a/apps/wallet/src/locales/fr.locale.ts b/apps/wallet/src/locales/fr.locale.ts index 0eb98ca59..5b13db30c 100644 --- a/apps/wallet/src/locales/fr.locale.ts +++ b/apps/wallet/src/locales/fr.locale.ts @@ -673,6 +673,13 @@ export default { upgrade: 'Mettre à jour', install: 'Installer', }, + wasm_memory_persistence: { + label: 'Persistance de la mémoire Wasm', + keep: 'Conserver', + replace: 'Remplacer', + hint: 'Utilisez « Conserver » pour préserver la mémoire principale du canister lors de la mise à jour, comme requis par les canisters Motoko utilisant la persistance orthogonale améliorée.', + }, + skip_pre_upgrade: 'Ignorer le hook pre-upgrade', }, terms: { license: 'Licence', diff --git a/apps/wallet/src/locales/pt.locale.ts b/apps/wallet/src/locales/pt.locale.ts index e1816dff3..49fdab402 100644 --- a/apps/wallet/src/locales/pt.locale.ts +++ b/apps/wallet/src/locales/pt.locale.ts @@ -669,6 +669,13 @@ export default { upgrade: 'Atualizar', install: 'Instalar', }, + wasm_memory_persistence: { + label: 'Persistência da memória Wasm', + keep: 'Manter', + replace: 'Substituir', + hint: 'Use "Manter" para preservar a memória principal do canister na atualização, conforme exigido por canisters Motoko que usam Persistência Ortogonal Aprimorada.', + }, + skip_pre_upgrade: 'Ignorar o hook pre-upgrade', }, terms: { license: 'Licença', diff --git a/apps/wallet/src/types/station.types.ts b/apps/wallet/src/types/station.types.ts index 0b5598850..124f461b9 100644 --- a/apps/wallet/src/types/station.types.ts +++ b/apps/wallet/src/types/station.types.ts @@ -9,6 +9,14 @@ import { ListExternalCanistersSortInput, } from '~/generated/station/station.did'; +/** + * The wasm memory persistence value nested inside the `upgrade` variant of + * `CanisterInstallMode`. Candid inlines this variant, so the generated + * bindings expose no named type for it; this mirrors the candid definition and + * is validated wherever it is assigned into a `CanisterInstallMode`. + */ +export type WasmMemoryPersistence = { keep: null } | { replace: null }; + export enum AccountTransferStatus { Created = 'created', Failed = 'failed', diff --git a/tests/integration/src/dfx_orbit/install.rs b/tests/integration/src/dfx_orbit/install.rs index fc40fa73b..7f90fa94e 100644 --- a/tests/integration/src/dfx_orbit/install.rs +++ b/tests/integration/src/dfx_orbit/install.rs @@ -8,7 +8,9 @@ use crate::{ TestEnv, }; use candid::Encode; -use dfx_orbit::canister::{CanisterInstallModeArgs, RequestCanisterInstallArgs}; +use dfx_orbit::canister::{ + CanisterInstallModeArgs, RequestCanisterInstallArgs, WasmMemoryPersistenceArgs, +}; use dfx_orbit::{ args::{RequestArgs, RequestArgsActions, VerifyArgs, VerifyArgsAction}, canister::{ @@ -17,7 +19,10 @@ use dfx_orbit::{ }, }; use sha2::{Digest, Sha256}; -use station_api::{GetRequestInput, RequestApprovalStatusDTO}; +use station_api::{ + CanisterInstallMode, GetRequestInput, RequestApprovalStatusDTO, RequestOperationDTO, + WasmMemoryPersistence, +}; use std::io::Write; use tempfile::NamedTempFile; @@ -89,6 +94,8 @@ fn canister_install(use_chunks: bool) { argument: None, arg_file: None, asset_canister: asset_canister.map(|p| p.to_text()), + wasm_memory_persistence: None, + skip_pre_upgrade: false, }; let request = dfx_orbit_test(&mut env, config, async { @@ -152,3 +159,96 @@ fn canister_install(use_chunks: bool) { let status = canister_status(&env, Some(canister_ids.station), test_canister); assert_eq!(status.module_hash, Some(module_hash)); } + +/// Test that an upgrade request created with `--wasm-memory-persistence keep` +/// carries the option through the station round-trip and that `verify` +/// accepts it. +#[test] +fn canister_upgrade_wasm_memory_persistence() { + let TestEnv { + mut env, + canister_ids, + .. + } = setup_new_env(); + + let (_dfx_user, _) = setup_dfx_user(&env, &canister_ids); + + // create the test canister + let test_canister = create_canister(&env, canister_ids.station); + + permit_change_operation(&env, &canister_ids); + set_four_eyes_on_change(&env, &canister_ids); + + let config = DfxOrbitTestConfig { + canister_ids: vec![(String::from("test"), test_canister)], + ..Default::default() + }; + + let mut wasm = NamedTempFile::new().unwrap(); + let module_bytes = get_canister_wasm("test_canister"); + wasm.write_all(&module_bytes).unwrap(); + + let inner_args = RequestCanisterInstallArgs { + canister: String::from("test"), + mode: CanisterInstallModeArgs::Upgrade, + wasm: wasm.path().as_os_str().to_str().unwrap().to_string(), + argument: None, + arg_file: None, + asset_canister: None, + wasm_memory_persistence: Some(WasmMemoryPersistenceArgs::Keep), + skip_pre_upgrade: false, + }; + + dfx_orbit_test(&mut env, config, async { + // Setup the station agent + let dfx_orbit = setup_dfx_orbit(canister_ids.station).await; + + let request = RequestArgs { + title: None, + summary: None, + action: RequestArgsActions::Canister(RequestCanisterArgs { + action: RequestCanisterActionArgs::Install(inner_args.clone()), + }), + } + .into_request(&dfx_orbit) + .await + .unwrap(); + + let request = dfx_orbit.station.request(request.clone()).await.unwrap(); + + let req_response = dfx_orbit + .station + .review_id(GetRequestInput { + request_id: request.request.id.clone(), + with_full_info: Some(false), + }) + .await + .unwrap(); + + // The stored operation must retain the `keep` persistence option. + let RequestOperationDTO::ChangeExternalCanister(op) = &req_response.request.operation + else { + panic!("expected a change external canister operation"); + }; + assert!(matches!( + &op.mode, + CanisterInstallMode::Upgrade(Some(opts)) + if opts.wasm_memory_persistence == Some(WasmMemoryPersistence::Keep) + )); + + // Verifying with the same options must succeed. + VerifyArgs { + request_id: request.request.id.clone(), + and_approve: false, + or_reject: false, + action: VerifyArgsAction::Canister(VerifyCanisterArgs { + action: VerifyCanisterActionArgs::Install(inner_args), + }), + } + .verify(&dfx_orbit, &req_response) + .await + .unwrap(); + + request.request + }); +} diff --git a/tools/dfx-orbit/README.md b/tools/dfx-orbit/README.md index 6824fba9d..348b0924f 100644 --- a/tools/dfx-orbit/README.md +++ b/tools/dfx-orbit/README.md @@ -141,6 +141,19 @@ Then a verifier can verify this request, using: dfx-orbit verify [REQUEST_ID] canister install --mode upgrade [CANISTER_NAME] --wasm [WASM_PATH] ``` +##### Enhanced Orthogonal Persistence (Motoko) + +Motoko canisters that use Enhanced Orthogonal Persistence must be upgraded with +`--wasm-memory-persistence keep`, otherwise the IC clears their main memory: + +``` +dfx-orbit request canister install --mode upgrade --wasm-memory-persistence keep [CANISTER_NAME] --wasm [WASM_PATH] +``` + +If the existing `pre_upgrade` hook traps and needs to be bypassed during a +recovery upgrade, add `--skip-pre-upgrade`. Both flags are only valid together +with `--mode upgrade`, and must also be repeated when verifying the request. + ### Upload assets to a canister We will assume that Orbit is a controller of the asset canister. diff --git a/tools/dfx-orbit/src/canister.rs b/tools/dfx-orbit/src/canister.rs index 8f672f9b7..3e370d742 100644 --- a/tools/dfx-orbit/src/canister.rs +++ b/tools/dfx-orbit/src/canister.rs @@ -9,7 +9,8 @@ mod util; pub use self::{ call::RequestCanisterCallArgs, install::CanisterInstallModeArgs, - install::RequestCanisterInstallArgs, settings::RequestCanisterUpdateSettingsArgs, + install::RequestCanisterInstallArgs, install::WasmMemoryPersistenceArgs, + settings::RequestCanisterUpdateSettingsArgs, }; // TODO: Support Canister create + integration test diff --git a/tools/dfx-orbit/src/canister/install.rs b/tools/dfx-orbit/src/canister/install.rs index d4978e210..98d9422d6 100644 --- a/tools/dfx-orbit/src/canister/install.rs +++ b/tools/dfx-orbit/src/canister/install.rs @@ -6,8 +6,9 @@ use clap::{Parser, ValueEnum}; use orbit_essentials::types::WasmModuleExtraChunks; use sha2::{Digest, Sha256}; use station_api::{ - CanisterInstallMode, ChangeExternalCanisterOperationDTO, ChangeExternalCanisterOperationInput, - GetRequestResponse, RequestOperationDTO, RequestOperationInput, + CanisterInstallMode, CanisterUpgradeOptionsInput, ChangeExternalCanisterOperationDTO, + ChangeExternalCanisterOperationInput, GetRequestResponse, RequestOperationDTO, + RequestOperationInput, WasmMemoryPersistence, }; use std::{collections::HashMap, fmt::Write, path::PathBuf}; @@ -31,6 +32,16 @@ pub struct RequestCanisterInstallArgs { /// The asset canister name or ID to upload module chunks to. #[clap(long)] pub asset_canister: Option, + /// The Wasm memory persistence mode to use when upgrading. `keep` is + /// required for Motoko canisters that use Enhanced Orthogonal Persistence, + /// otherwise the IC clears their main memory on upgrade. Only valid with + /// `--mode upgrade`. + #[clap(long, value_enum, rename_all = "kebab-case")] + pub wasm_memory_persistence: Option, + /// Skip the canister's `pre_upgrade` hook during the upgrade. Only valid + /// with `--mode upgrade`. + #[clap(long)] + pub skip_pre_upgrade: bool, } #[derive(CandidType)] @@ -57,7 +68,7 @@ impl RequestCanisterInstallArgs { let canister_id = dfx_orbit.canister_id(&self.canister)?; let (module, arg) = self.load_module_and_args()?; - let mode = self.mode.into(); + let mode = self.install_mode()?; let (module, module_extra_chunks) = if let Some(ref asset_canister) = self.asset_canister { let asset_canister_id = dfx_orbit.canister_id(asset_canister)?; @@ -127,8 +138,12 @@ impl RequestCanisterInstallArgs { op.canister_id ); } - if CanisterInstallModeArgs::from(op.mode.clone()) != self.mode { - bail!("Canister install mode {:?} does not match", op.mode); + let expected_mode = self.install_mode()?; + if !install_modes_match(&op.mode, &expected_mode) { + bail!( + "Canister install mode {:?} does not match expected {expected_mode:?}", + op.mode + ); } if op.module_checksum != module_checksum { log_hashes( @@ -160,6 +175,57 @@ impl RequestCanisterInstallArgs { Ok((module, args)) } + + /// Builds the Orbit API install mode from the CLI arguments, embedding the + /// upgrade options (`--wasm-memory-persistence` / `--skip-pre-upgrade`) + /// into the `upgrade` variant. Those options are only meaningful for + /// upgrades, so combining them with any other mode is rejected. + fn install_mode(&self) -> anyhow::Result { + let upgrade_options = self.upgrade_options(); + match self.mode { + CanisterInstallModeArgs::Install => { + if upgrade_options.is_some() { + bail!("`--wasm-memory-persistence` and `--skip-pre-upgrade` are only valid with `--mode upgrade`"); + } + Ok(CanisterInstallMode::Install) + } + CanisterInstallModeArgs::Reinstall => { + if upgrade_options.is_some() { + bail!("`--wasm-memory-persistence` and `--skip-pre-upgrade` are only valid with `--mode upgrade`"); + } + Ok(CanisterInstallMode::Reinstall) + } + CanisterInstallModeArgs::Upgrade => Ok(CanisterInstallMode::Upgrade(upgrade_options)), + } + } + + /// Collects the upgrade options from the CLI flags, returning `None` when + /// no flag was provided so the request matches one created without them. + fn upgrade_options(&self) -> Option { + let wasm_memory_persistence = self.wasm_memory_persistence.map(Into::into); + let skip_pre_upgrade = self.skip_pre_upgrade.then_some(true); + + if wasm_memory_persistence.is_none() && skip_pre_upgrade.is_none() { + None + } else { + Some(CanisterUpgradeOptionsInput { + wasm_memory_persistence, + skip_pre_upgrade, + }) + } + } +} + +/// Compares two install modes, treating the `upgrade` options as significant. +fn install_modes_match(actual: &CanisterInstallMode, expected: &CanisterInstallMode) -> bool { + match (actual, expected) { + (CanisterInstallMode::Install, CanisterInstallMode::Install) => true, + (CanisterInstallMode::Reinstall, CanisterInstallMode::Reinstall) => true, + (CanisterInstallMode::Upgrade(actual), CanisterInstallMode::Upgrade(expected)) => { + actual == expected + } + _ => false, + } } /// Canister installation mode equivalent to `dfx canister install --mode XXX` and `orbit_station_api::CanisterInstallMode`. @@ -193,6 +259,35 @@ impl From for CanisterInstallModeArgs { } } +/// Wasm memory persistence mode equivalent to `dfx canister install +/// --wasm-memory-persistence XXX` and `orbit_station_api::WasmMemoryPersistence`. +#[derive(Copy, Clone, Eq, PartialEq, Debug, ValueEnum)] +pub enum WasmMemoryPersistenceArgs { + /// Preserve the canister's main memory (required for Motoko canisters that + /// use Enhanced Orthogonal Persistence). + Keep, + /// Clear the canister's main memory. + Replace, +} + +impl From for WasmMemoryPersistence { + fn from(value: WasmMemoryPersistenceArgs) -> Self { + match value { + WasmMemoryPersistenceArgs::Keep => Self::Keep, + WasmMemoryPersistenceArgs::Replace => Self::Replace, + } + } +} + +impl From for WasmMemoryPersistenceArgs { + fn from(value: WasmMemoryPersistence) -> Self { + match value { + WasmMemoryPersistence::Keep => Self::Keep, + WasmMemoryPersistence::Replace => Self::Replace, + } + } +} + impl DfxOrbit { pub(crate) fn display_change_canister_operation( &self, @@ -213,6 +308,19 @@ impl DfxOrbit { }; writeln!(output, "Mode: {mode}")?; + if let CanisterInstallMode::Upgrade(Some(opts)) = &op.mode { + if let Some(persistence) = &opts.wasm_memory_persistence { + let persistence = match persistence { + WasmMemoryPersistence::Keep => "keep", + WasmMemoryPersistence::Replace => "replace", + }; + writeln!(output, "Wasm memory persistence: {persistence}")?; + } + if let Some(skip_pre_upgrade) = opts.skip_pre_upgrade { + writeln!(output, "Skip pre-upgrade: {skip_pre_upgrade}")?; + } + } + writeln!(output, "Module checksum: {}", &op.module_checksum)?; if let Some(arg_checksum) = &op.arg_checksum { writeln!(output, "Argument checksum: {arg_checksum}")?; @@ -220,3 +328,130 @@ impl DfxOrbit { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn args( + mode: CanisterInstallModeArgs, + wasm_memory_persistence: Option, + skip_pre_upgrade: bool, + ) -> RequestCanisterInstallArgs { + RequestCanisterInstallArgs { + canister: String::from("test"), + mode, + wasm: String::from("test.wasm"), + argument: None, + arg_file: None, + asset_canister: None, + wasm_memory_persistence, + skip_pre_upgrade, + } + } + + #[test] + fn upgrade_without_flags_maps_to_none() { + let mode = args(CanisterInstallModeArgs::Upgrade, None, false) + .install_mode() + .unwrap(); + assert!(matches!(mode, CanisterInstallMode::Upgrade(None))); + } + + #[test] + fn upgrade_with_wasm_memory_persistence() { + // `station_api::CanisterInstallMode` does not implement `PartialEq`, so + // we destructure and compare the (comparable) upgrade options. + let mode = args( + CanisterInstallModeArgs::Upgrade, + Some(WasmMemoryPersistenceArgs::Keep), + false, + ) + .install_mode() + .unwrap(); + let CanisterInstallMode::Upgrade(Some(opts)) = mode else { + panic!("expected upgrade with options, got {mode:?}"); + }; + assert_eq!( + opts, + CanisterUpgradeOptionsInput { + wasm_memory_persistence: Some(WasmMemoryPersistence::Keep), + skip_pre_upgrade: None, + } + ); + } + + #[test] + fn upgrade_with_skip_pre_upgrade() { + let mode = args(CanisterInstallModeArgs::Upgrade, None, true) + .install_mode() + .unwrap(); + let CanisterInstallMode::Upgrade(Some(opts)) = mode else { + panic!("expected upgrade with options, got {mode:?}"); + }; + assert_eq!( + opts, + CanisterUpgradeOptionsInput { + wasm_memory_persistence: None, + skip_pre_upgrade: Some(true), + } + ); + } + + #[test] + fn upgrade_flags_rejected_for_install() { + assert!(args( + CanisterInstallModeArgs::Install, + Some(WasmMemoryPersistenceArgs::Keep), + false, + ) + .install_mode() + .is_err()); + } + + #[test] + fn upgrade_flags_rejected_for_reinstall() { + assert!(args(CanisterInstallModeArgs::Reinstall, None, true) + .install_mode() + .is_err()); + } + + #[test] + fn plain_install_and_reinstall_have_no_options() { + assert!(matches!( + args(CanisterInstallModeArgs::Install, None, false) + .install_mode() + .unwrap(), + CanisterInstallMode::Install + )); + assert!(matches!( + args(CanisterInstallModeArgs::Reinstall, None, false) + .install_mode() + .unwrap(), + CanisterInstallMode::Reinstall + )); + } + + #[test] + fn modes_match_compares_upgrade_options() { + let keep = CanisterInstallMode::Upgrade(Some(CanisterUpgradeOptionsInput { + wasm_memory_persistence: Some(WasmMemoryPersistence::Keep), + skip_pre_upgrade: None, + })); + let replace = CanisterInstallMode::Upgrade(Some(CanisterUpgradeOptionsInput { + wasm_memory_persistence: Some(WasmMemoryPersistence::Replace), + skip_pre_upgrade: None, + })); + + assert!(install_modes_match(&keep, &keep)); + assert!(!install_modes_match(&keep, &replace)); + assert!(!install_modes_match( + &CanisterInstallMode::Upgrade(None), + &keep + )); + assert!(!install_modes_match( + &CanisterInstallMode::Install, + &CanisterInstallMode::Reinstall + )); + } +}