From 2a24fc60f50de3ef1a7378aa456bb6d96fbe51c9 Mon Sep 17 00:00:00 2001 From: Raymond Khalife Date: Mon, 19 Jan 2026 19:41:03 -0500 Subject: [PATCH 1/4] fix: Allow overloading the local environment --- crates/icp/src/lib.rs | 135 +++++++++++++++++++++++++ crates/icp/src/manifest/environment.rs | 24 +---- 2 files changed, 140 insertions(+), 19 deletions(-) diff --git a/crates/icp/src/lib.rs b/crates/icp/src/lib.rs index 0cddc242..6bcf4177 100644 --- a/crates/icp/src/lib.rs +++ b/crates/icp/src/lib.rs @@ -510,3 +510,138 @@ impl ProjectLoad for NoProjectLoader { Ok(false) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::canister::recipe::{Resolve, ResolveError}; + use crate::manifest::{ + ProjectRootLocate, ProjectRootLocateError, + canister::{BuildSteps, SyncSteps}, + recipe::Recipe, + }; + use camino_tempfile::Utf8TempDir; + use indoc::indoc; + + struct MockProjectRootLocate { + path: PathBuf, + } + + impl MockProjectRootLocate { + fn new(path: PathBuf) -> Self { + Self { path } + } + } + + impl ProjectRootLocate for MockProjectRootLocate { + fn locate(&self) -> Result { + Ok(self.path.clone()) + } + } + + struct MockRecipeResolver; + + #[async_trait] + impl Resolve for MockRecipeResolver { + async fn resolve(&self, _recipe: &Recipe) -> Result<(BuildSteps, SyncSteps), ResolveError> { + use crate::manifest::adapter::prebuilt::{Adapter as PrebuiltAdapter, LocalSource, SourceField}; + use crate::manifest::canister::BuildStep; + + // Create a minimal BuildSteps with a dummy prebuilt step + let build_steps = BuildSteps { + steps: vec![BuildStep::Prebuilt(PrebuiltAdapter { + source: SourceField::Local(LocalSource { + path: "dummy.wasm".into(), + }), + sha256: None, + })], + }; + + Ok((build_steps, SyncSteps::default())) + } + } + + #[tokio::test] + async fn test_load_minimal_project() { + // Create temp directory with icp.yaml + let temp_dir = Utf8TempDir::new().unwrap(); + let project_dir = temp_dir.path(); + + // Write a minimal icp.yaml + let manifest_content = indoc! {r#" + canisters: + - name: backend + build: + steps: + - type: pre-built + path: backend.wasm + "#}; + std::fs::write(project_dir.join("icp.yaml"), manifest_content).unwrap(); + + // Create ProjectLoadImpl with mocks + let loader = ProjectLoadImpl { + project_root_locate: Arc::new(MockProjectRootLocate::new(project_dir.to_path_buf())), + recipe: Arc::new(MockRecipeResolver), + }; + + // Call load + let result = loader.load().await; + + // Assert success and check project contents + assert!(result.is_ok()); + let project = result.unwrap(); + assert_eq!(project.dir, project_dir); + assert!(project.canisters.contains_key("backend"), "The backend canister was not found"); + assert!(project.environments.contains_key("local"), "The default `local` environment was not injected"); + assert!(project.environments.contains_key("ic"), "The default `ic` environment was not injected"); + assert!(project.networks.contains_key("local"), "The default `local` network was not injected"); + assert!(project.networks.contains_key("ic"), "The default `ic` network was not injected"); + } + + #[tokio::test] + async fn test_load_project_local_override() { + // Create temp directory with icp.yaml + let temp_dir = Utf8TempDir::new().unwrap(); + let project_dir = temp_dir.path(); + + // Write a minimal icp.yaml + let manifest_content = indoc! {r#" + networks: + - name: test-network + mode: connected + url: https://somenetwork.icp + environments: + - name: local + network: test-network + canisters: + - name: backend + build: + steps: + - type: pre-built + path: backend.wasm + "#}; + std::fs::write(project_dir.join("icp.yaml"), manifest_content).unwrap(); + + // Create ProjectLoadImpl with mocks + let loader = ProjectLoadImpl { + project_root_locate: Arc::new(MockProjectRootLocate::new(project_dir.to_path_buf())), + recipe: Arc::new(MockRecipeResolver), + }; + + // Call load + let result = loader.load().await; + + // Assert success and check project contents + assert!(result.is_ok(), "The project did not load: {:?}", result); + let project = result.unwrap(); + assert_eq!(project.dir, project_dir); + assert!(project.canisters.contains_key("backend"), "The backend canister was not found"); + assert!(project.environments.contains_key("local"), "The default `local` environment was not injected"); + let e = project.environments.get("local").unwrap(); + assert_eq!(e.network.name, "test-network"); + assert!(project.environments.contains_key("ic"), "The default `ic` environment was not injected"); + assert!(project.networks.contains_key("local"), "The default `local` network was not injected"); + assert!(project.networks.contains_key("ic"), "The default `ic` network was not injected"); + } + +} diff --git a/crates/icp/src/manifest/environment.rs b/crates/icp/src/manifest/environment.rs index b4aad4f7..c582e235 100644 --- a/crates/icp/src/manifest/environment.rs +++ b/crates/icp/src/manifest/environment.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use schemars::JsonSchema; use serde::{Deserialize, Deserializer}; -use snafu::prelude::*; use crate::{canister::Settings, prelude::LOCAL}; @@ -52,16 +51,8 @@ pub struct EnvironmentManifest { pub init_args: Option>, } -#[derive(Debug, Snafu)] -pub enum ParseError { - #[snafu(display("Overriding the local environment is not supported."))] - OverrideLocal, -} - -impl TryFrom for EnvironmentManifest { - type Error = ParseError; - - fn try_from(v: EnvironmentInner) -> Result { +impl From for EnvironmentManifest { + fn from(v: EnvironmentInner) -> Self { let EnvironmentInner { name, network, @@ -70,11 +61,6 @@ impl TryFrom for EnvironmentManifest { init_args, } = v; - // Name - if name == LOCAL { - return OverrideLocalSnafu.fail(); - } - // Network let network = network.unwrap_or(LOCAL.to_string()); @@ -93,7 +79,7 @@ impl TryFrom for EnvironmentManifest { None => CanisterSelection::Everything, }; - Ok(Self { + Self { name, network, canisters, @@ -101,14 +87,14 @@ impl TryFrom for EnvironmentManifest { // Keep as-is, setting overrides is optional settings, init_args, - }) + } } } impl<'de> Deserialize<'de> for EnvironmentManifest { fn deserialize>(d: D) -> Result { let inner: EnvironmentInner = Deserialize::deserialize(d)?; - inner.try_into().map_err(serde::de::Error::custom) + Ok(inner.into()) } } From ad638f99c4ccfab336517533a073731bb74cea61 Mon Sep 17 00:00:00 2001 From: Raymond Khalife Date: Mon, 19 Jan 2026 20:32:49 -0500 Subject: [PATCH 2/4] fmt --- crates/icp/src/lib.rs | 55 +++++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/crates/icp/src/lib.rs b/crates/icp/src/lib.rs index 6bcf4177..ee72ae4c 100644 --- a/crates/icp/src/lib.rs +++ b/crates/icp/src/lib.rs @@ -544,7 +544,9 @@ mod tests { #[async_trait] impl Resolve for MockRecipeResolver { async fn resolve(&self, _recipe: &Recipe) -> Result<(BuildSteps, SyncSteps), ResolveError> { - use crate::manifest::adapter::prebuilt::{Adapter as PrebuiltAdapter, LocalSource, SourceField}; + use crate::manifest::adapter::prebuilt::{ + Adapter as PrebuiltAdapter, LocalSource, SourceField, + }; use crate::manifest::canister::BuildStep; // Create a minimal BuildSteps with a dummy prebuilt step @@ -591,11 +593,26 @@ mod tests { assert!(result.is_ok()); let project = result.unwrap(); assert_eq!(project.dir, project_dir); - assert!(project.canisters.contains_key("backend"), "The backend canister was not found"); - assert!(project.environments.contains_key("local"), "The default `local` environment was not injected"); - assert!(project.environments.contains_key("ic"), "The default `ic` environment was not injected"); - assert!(project.networks.contains_key("local"), "The default `local` network was not injected"); - assert!(project.networks.contains_key("ic"), "The default `ic` network was not injected"); + assert!( + project.canisters.contains_key("backend"), + "The backend canister was not found" + ); + assert!( + project.environments.contains_key("local"), + "The default `local` environment was not injected" + ); + assert!( + project.environments.contains_key("ic"), + "The default `ic` environment was not injected" + ); + assert!( + project.networks.contains_key("local"), + "The default `local` network was not injected" + ); + assert!( + project.networks.contains_key("ic"), + "The default `ic` network was not injected" + ); } #[tokio::test] @@ -635,13 +652,27 @@ mod tests { assert!(result.is_ok(), "The project did not load: {:?}", result); let project = result.unwrap(); assert_eq!(project.dir, project_dir); - assert!(project.canisters.contains_key("backend"), "The backend canister was not found"); - assert!(project.environments.contains_key("local"), "The default `local` environment was not injected"); + assert!( + project.canisters.contains_key("backend"), + "The backend canister was not found" + ); + assert!( + project.environments.contains_key("local"), + "The default `local` environment was not injected" + ); let e = project.environments.get("local").unwrap(); assert_eq!(e.network.name, "test-network"); - assert!(project.environments.contains_key("ic"), "The default `ic` environment was not injected"); - assert!(project.networks.contains_key("local"), "The default `local` network was not injected"); - assert!(project.networks.contains_key("ic"), "The default `ic` network was not injected"); + assert!( + project.environments.contains_key("ic"), + "The default `ic` environment was not injected" + ); + assert!( + project.networks.contains_key("local"), + "The default `local` network was not injected" + ); + assert!( + project.networks.contains_key("ic"), + "The default `ic` network was not injected" + ); } - } From 6ea3fda3d3804770de87762c6b0c617fcf3a8a04 Mon Sep 17 00:00:00 2001 From: Raymond Khalife Date: Mon, 19 Jan 2026 20:35:06 -0500 Subject: [PATCH 3/4] remove old test --- crates/icp/src/manifest/environment.rs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/crates/icp/src/manifest/environment.rs b/crates/icp/src/manifest/environment.rs index c582e235..358dd34c 100644 --- a/crates/icp/src/manifest/environment.rs +++ b/crates/icp/src/manifest/environment.rs @@ -120,21 +120,4 @@ mod tests { }, ); } - - #[test] - fn override_local() { - match serde_yaml::from_str::(r#"name: local"#) { - // No Error - Ok(_) => { - panic!("an environment named local should result in an error"); - } - - // Wrong Error - Err(err) => { - if !format!("{err}").starts_with("Overriding the local environment") { - panic!("an environment named local resulted in the wrong error: {err}"); - }; - } - }; - } } From 05c49c092e728b1182d800785b2bab7eb2e26b9d Mon Sep 17 00:00:00 2001 From: Raymond Khalife Date: Tue, 20 Jan 2026 10:49:25 -0500 Subject: [PATCH 4/4] update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0db14a23..0983ff5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,9 @@ * feat: `icp canister metadata ` now fetches metadata sections from specified canisters * fix: Validate explicit canister paths and throw an error if `canister.yaml` is not found * feat!: Rename the implicit "mainnet" network to "ic" - * The corresponding environment "ic" is defined implicitly which can be overwritten by user configuration + * The corresponding environment "ic" is defined implicitly which can be overwritten by user configuration. * The `--mainnet` and `--ic` flags are removed. Use `-n/--network ic`, `-e/--environment ic` instead. +* feat: Allow overriding the implicit `local` network and environment. # v0.1.0-beta.3