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
+ ));
+ }
+}