use super::*;
use crate::release_set::ROOT_RELEASE_SET_MANIFEST_FILE;
use crate::test_support::temp_dir;
use serde::Serialize;
use std::{fs, path::Path};
#[test]
fn plan_round_trips_through_json() {
let plan = DeploymentPlanV1 {
schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
plan_id: "plan-local-root".to_string(),
deployment_identity: sample_identity(),
trust_domain: TrustDomainV1 {
root_trust_anchor: Some("aaaaa-aa".to_string()),
migration_from: None,
},
fleet_template: "root".to_string(),
runtime_variant: "local".to_string(),
authority_profile: AuthorityProfileV1 {
profile_id: "local-default".to_string(),
expected_controllers: vec!["aaaaa-aa".to_string()],
staging_controllers: Vec::new(),
emergency_controllers: Vec::new(),
},
role_artifacts: vec![sample_role_artifact()],
expected_canisters: vec![ExpectedCanisterV1 {
role: "root".to_string(),
canister_id: Some("aaaaa-aa".to_string()),
control_class: CanisterControlClassV1::DeploymentControlled,
}],
expected_pool: Vec::new(),
expected_verifier_readiness: VerifierReadinessExpectationV1 {
required: true,
expected_role_epochs: vec![RoleEpochExpectationV1 {
role: "root".to_string(),
minimum_epoch: 1,
}],
},
unresolved_assumptions: Vec::new(),
};
let encoded = serde_json::to_string(&plan).expect("plan should encode");
let decoded = serde_json::from_str::<DeploymentPlanV1>(&encoded).expect("plan should decode");
assert_eq!(decoded, plan);
}
#[test]
fn inventory_round_trips_through_json() {
let inventory = DeploymentInventoryV1 {
schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
inventory_id: "inventory-1".to_string(),
observed_at: "2026-05-21T00:00:00Z".to_string(),
observed_identity: Some(sample_identity()),
local_config: LocalDeploymentConfigV1 {
config_path: Some("icp.yml".to_string()),
raw_config_sha256: Some("raw".to_string()),
canonical_embedded_config_sha256: Some("canonical".to_string()),
},
observed_canisters: vec![ObservedCanisterV1 {
canister_id: "aaaaa-aa".to_string(),
role: Some("root".to_string()),
control_class: CanisterControlClassV1::DeploymentControlled,
controllers: vec!["aaaaa-aa".to_string()],
module_hash: Some("module".to_string()),
status: Some("running".to_string()),
root_trust_anchor: Some("aaaaa-aa".to_string()),
canonical_embedded_config_digest: Some("canonical".to_string()),
role_assignment_source: Some("registry".to_string()),
}],
observed_pool: Vec::new(),
observed_artifacts: vec![ObservedArtifactV1 {
role: "root".to_string(),
artifact_path: ".icp/local/canisters/root/root.wasm.gz".to_string(),
file_sha256: Some("artifact-file".to_string()),
file_sha256_source: Some(ArtifactDigestSourceV1::ObservedFileDigest),
payload_sha256: Some("artifact".to_string()),
payload_size_bytes: Some(42),
source: ArtifactSourceV1::LocalBuild,
}],
observed_verifier_readiness: VerifierReadinessObservationV1 {
status: ObservationStatusV1::Observed,
role_epochs: vec![RoleEpochObservationV1 {
role: "root".to_string(),
observed_epoch: Some(1),
status: ObservationStatusV1::Observed,
}],
},
unresolved_observations: Vec::new(),
};
let encoded = serde_json::to_string_pretty(&inventory).expect("inventory should encode");
let decoded =
serde_json::from_str::<DeploymentInventoryV1>(&encoded).expect("inventory should decode");
assert_eq!(decoded, inventory);
}
#[test]
fn receipt_diff_and_safety_report_support_not_evaluated_state() {
let receipt = DeploymentReceiptV1 {
schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
operation_id: "operation-1".to_string(),
plan_id: "plan-local-root".to_string(),
started_at: "2026-05-21T00:00:00Z".to_string(),
finished_at: None,
operator_principal: None,
root_principal: Some("aaaaa-aa".to_string()),
previous_observed_deployment_epoch: None,
phase_receipts: vec![PhaseReceiptV1 {
phase: "build_artifacts".to_string(),
started_at: "2026-05-21T00:00:00Z".to_string(),
finished_at: None,
attempted_action: "build root artifact".to_string(),
verified_postcondition: VerifiedPostconditionV1 {
status: ObservationStatusV1::NotObserved,
evidence: Vec::new(),
},
}],
final_inventory_id: None,
command_result: DeploymentCommandResultV1::NotFinished,
};
let diff = DeploymentDiffV1 {
schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
plan_identity: sample_identity(),
observed_identity: None,
artifact_diff: Vec::new(),
controller_diff: Vec::new(),
pool_diff: Vec::new(),
embedded_config_diff: Vec::new(),
module_hash_diff: Vec::new(),
verifier_readiness_diff: Vec::new(),
resume_safety: ResumeSafetyV1 {
status: SafetyStatusV1::NotEvaluated,
reasons: vec!["inventory not collected".to_string()],
},
hard_failures: Vec::new(),
warnings: Vec::new(),
resumable_phases: Vec::new(),
};
let report = SafetyReportV1 {
schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
report_id: "report-1".to_string(),
diff_id: None,
status: SafetyStatusV1::NotEvaluated,
summary: "deployment safety has not been evaluated".to_string(),
hard_failures: Vec::new(),
warnings: Vec::new(),
next_actions: vec!["collect deployment inventory".to_string()],
};
assert_json_round_trip(&receipt);
assert_json_round_trip(&diff);
assert_json_round_trip(&report);
}
#[test]
fn local_check_builds_plan_inventory_diff_and_report() {
let temp = TempWorkspace::new("canic-host-local-check");
let workspace_root = temp.path().join("workspace");
let icp_root = temp.path().join("icp");
let config_dir = workspace_root.join("fleets");
fs::create_dir_all(&config_dir).expect("create config dir");
fs::write(config_dir.join("canic.toml"), SAMPLE_CONFIG).expect("write config");
write_artifact(&icp_root, "root", b"root-artifact");
write_release_set_manifest(&icp_root);
let check = check_local_deployment(&LocalDeploymentCheckRequest {
deployment_name: "demo".to_string(),
network: "local".to_string(),
workspace_root,
icp_root,
observed_at: "2026-05-21T00:00:00Z".to_string(),
runtime_variant: "local".to_string(),
build_profile: "fast".to_string(),
})
.expect("check local deployment");
assert_eq!(check.schema_version, DEPLOYMENT_TRUTH_SCHEMA_VERSION);
assert_eq!(check.check_id, "local:local:demo:check");
assert_eq!(check.plan.plan_id, "local:local:demo:plan");
assert_eq!(check.inventory.inventory_id, "local:local:demo");
assert_eq!(check.diff.resume_safety.status, check.report.status);
assert!(
check
.diff
.hard_failures
.iter()
.any(|finding| finding.code == "artifact_missing")
);
assert_eq!(check.report.status, SafetyStatusV1::Blocked);
}
#[test]
fn local_inventory_collects_configured_roles_and_artifacts_without_live_queries() {
let temp = TempWorkspace::new("canic-host-local-inventory");
let workspace_root = temp.path().join("workspace");
let icp_root = temp.path().join("icp");
let config_dir = workspace_root.join("fleets");
fs::create_dir_all(&config_dir).expect("create config dir");
fs::write(config_dir.join("canic.toml"), SAMPLE_CONFIG).expect("write config");
let artifact_path = icp_root
.join(".icp")
.join("local")
.join("canisters")
.join("root")
.join("root.wasm.gz");
fs::create_dir_all(artifact_path.parent().expect("artifact parent"))
.expect("create artifact dir");
fs::write(&artifact_path, b"artifact").expect("write artifact");
let inventory = collect_local_deployment_inventory(&LocalInventoryRequest {
deployment_name: "demo".to_string(),
network: "local".to_string(),
workspace_root,
icp_root,
observed_at: "2026-05-21T00:00:00Z".to_string(),
})
.expect("collect inventory");
assert_eq!(inventory.schema_version, DEPLOYMENT_TRUTH_SCHEMA_VERSION);
assert_eq!(inventory.inventory_id, "local:local:demo");
assert_eq!(inventory.observed_artifacts.len(), 1);
assert_eq!(inventory.observed_artifacts[0].role, "root");
assert_eq!(inventory.observed_artifacts[0].payload_size_bytes, Some(8));
assert_eq!(
inventory.observed_artifacts[0].file_sha256_source,
Some(ArtifactDigestSourceV1::ObservedFileDigest)
);
assert_eq!(
inventory.observed_artifacts[0]
.file_sha256
.as_ref()
.map(String::len),
Some(64)
);
assert!(
inventory
.unresolved_observations
.iter()
.any(|gap| gap.key == "local_artifacts.user_hub")
);
}
#[test]
fn local_inventory_reports_missing_config_as_observation_gap() {
let temp = TempWorkspace::new("canic-host-local-inventory-missing-config");
let inventory = collect_local_deployment_inventory(&LocalInventoryRequest {
deployment_name: "demo".to_string(),
network: "local".to_string(),
workspace_root: temp.path().join("workspace"),
icp_root: temp.path().join("icp"),
observed_at: "2026-05-21T00:00:00Z".to_string(),
})
.expect("collect inventory");
assert_eq!(inventory.inventory_id, "local:local:demo");
assert!(
inventory
.unresolved_observations
.iter()
.any(|gap| gap.key == "local_config.fleet_name")
);
assert!(
inventory
.unresolved_observations
.iter()
.any(|gap| gap.key == "local_config.roles")
);
}
#[test]
fn local_artifact_manifest_collects_roles_and_release_set_hashes() {
let temp = TempWorkspace::new("canic-host-local-artifact-manifest");
let workspace_root = temp.path().join("workspace");
let icp_root = temp.path().join("icp");
let config_dir = workspace_root.join("fleets");
fs::create_dir_all(&config_dir).expect("create config dir");
fs::write(config_dir.join("canic.toml"), SAMPLE_CONFIG).expect("write config");
write_artifact(&icp_root, "root", b"root-artifact");
write_artifact(&icp_root, "user_hub", b"user-hub-artifact");
write_release_set_manifest(&icp_root);
let manifest = collect_local_role_artifact_manifest(&LocalArtifactManifestRequest {
network: "local".to_string(),
workspace_root,
icp_root,
});
assert_eq!(manifest.manifest_id, "local:local:demo:artifacts");
assert_eq!(manifest.role_artifacts.len(), 2);
let user_hub = manifest
.role_artifacts
.iter()
.find(|artifact| artifact.role == "user_hub")
.expect("user_hub artifact");
assert_eq!(user_hub.wasm_gz_sha256.as_deref(), Some("user-hub-hash"));
assert_eq!(
user_hub.wasm_gz_sha256_source,
Some(ArtifactDigestSourceV1::ReleaseSetManifest)
);
assert_eq!(user_hub.wasm_gz_size_bytes, Some(17));
assert_eq!(
user_hub.observed_wasm_gz_file_sha256_source,
Some(ArtifactDigestSourceV1::ObservedFileDigest)
);
assert_eq!(
user_hub
.observed_wasm_gz_file_sha256
.as_ref()
.map(String::len),
Some(64)
);
let root = manifest
.role_artifacts
.iter()
.find(|artifact| artifact.role == "root")
.expect("root artifact");
assert_eq!(root.wasm_gz_sha256, None);
assert_eq!(root.wasm_gz_sha256_source, None);
assert!(manifest.unresolved_artifacts.is_empty());
}
#[test]
fn local_artifact_manifest_reports_network_artifact_fallback() {
let temp = TempWorkspace::new("canic-host-local-artifact-manifest-fallback");
let workspace_root = temp.path().join("workspace");
let icp_root = temp.path().join("icp");
let config_dir = workspace_root.join("fleets");
fs::create_dir_all(&config_dir).expect("create config dir");
fs::write(config_dir.join("canic.toml"), SAMPLE_CONFIG).expect("write config");
write_artifact(&icp_root, "root", b"root-artifact");
let manifest = collect_local_role_artifact_manifest(&LocalArtifactManifestRequest {
network: "ic".to_string(),
workspace_root,
icp_root,
});
assert!(
manifest
.unresolved_artifacts
.iter()
.any(|gap| gap.key == "local_artifacts.network_fallback")
);
}
#[test]
fn local_artifact_manifest_records_missing_artifacts_as_gaps() {
let temp = TempWorkspace::new("canic-host-local-artifact-manifest-missing");
let workspace_root = temp.path().join("workspace");
let icp_root = temp.path().join("icp");
let config_dir = workspace_root.join("fleets");
fs::create_dir_all(&config_dir).expect("create config dir");
fs::write(config_dir.join("canic.toml"), SAMPLE_CONFIG).expect("write config");
write_artifact(&icp_root, "root", b"root-artifact");
let manifest = collect_local_role_artifact_manifest(&LocalArtifactManifestRequest {
network: "local".to_string(),
workspace_root,
icp_root,
});
assert!(
manifest
.unresolved_artifacts
.iter()
.any(|gap| gap.key == "local_artifacts.release_set_manifest")
);
assert!(
manifest
.unresolved_artifacts
.iter()
.any(|gap| gap.key == "local_artifacts.user_hub")
);
}
#[test]
fn local_plan_uses_configured_roles_and_local_artifact_manifest() {
let temp = TempWorkspace::new("canic-host-local-plan");
let workspace_root = temp.path().join("workspace");
let icp_root = temp.path().join("icp");
let config_dir = workspace_root.join("fleets");
fs::create_dir_all(&config_dir).expect("create config dir");
fs::write(config_dir.join("canic.toml"), SAMPLE_CONFIG).expect("write config");
write_artifact(&icp_root, "root", b"root-artifact");
write_artifact(&icp_root, "user_hub", b"user-hub-artifact");
write_release_set_manifest(&icp_root);
let plan = build_local_deployment_plan(&LocalDeploymentPlanRequest {
deployment_name: "demo-local".to_string(),
network: "local".to_string(),
workspace_root,
icp_root,
runtime_variant: "local".to_string(),
build_profile: "fast".to_string(),
});
assert_eq!(plan.plan_id, "local:local:demo-local:plan");
assert_eq!(plan.deployment_identity.deployment_name, "demo-local");
assert_eq!(plan.fleet_template, "demo");
assert_eq!(plan.runtime_variant, "local");
assert_eq!(plan.role_artifacts.len(), 2);
assert!(
plan.role_artifacts
.iter()
.all(|artifact| artifact.build_profile == "fast")
);
assert!(
plan.role_artifacts
.iter()
.any(|artifact| artifact.role == "user_hub"
&& artifact.wasm_gz_sha256.as_deref() == Some("user-hub-hash")
&& artifact.wasm_gz_sha256_source
== Some(ArtifactDigestSourceV1::ReleaseSetManifest)
&& artifact.observed_wasm_gz_file_sha256_source
== Some(ArtifactDigestSourceV1::ObservedFileDigest))
);
assert_eq!(
plan.expected_canisters
.iter()
.map(|canister| canister.role.as_str())
.collect::<Vec<_>>(),
vec!["root", "user_hub"]
);
assert!(plan.unresolved_assumptions.is_empty());
}
#[test]
fn deployment_diff_blocks_missing_artifacts_and_unsafe_control_class() {
let plan = sample_plan();
let inventory = DeploymentInventoryV1 {
schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
inventory_id: "inventory-1".to_string(),
observed_at: "2026-05-21T00:00:00Z".to_string(),
observed_identity: Some(sample_identity()),
local_config: LocalDeploymentConfigV1 {
config_path: Some("icp.yml".to_string()),
raw_config_sha256: None,
canonical_embedded_config_sha256: Some("runtime".to_string()),
},
observed_canisters: vec![ObservedCanisterV1 {
canister_id: "aaaaa-aa".to_string(),
role: Some("root".to_string()),
control_class: CanisterControlClassV1::UnknownUnsafe,
controllers: Vec::new(),
module_hash: None,
status: None,
root_trust_anchor: Some("aaaaa-aa".to_string()),
canonical_embedded_config_digest: None,
role_assignment_source: Some("local_install_state".to_string()),
}],
observed_pool: Vec::new(),
observed_artifacts: Vec::new(),
observed_verifier_readiness: VerifierReadinessObservationV1 {
status: ObservationStatusV1::Observed,
role_epochs: Vec::new(),
},
unresolved_observations: Vec::new(),
};
let diff = compare_plan_to_inventory(&plan, &inventory);
let report = safety_report_from_diff("report-1", Some("diff-1".to_string()), &diff);
assert_eq!(diff.resume_safety.status, SafetyStatusV1::Blocked);
assert!(
diff.hard_failures
.iter()
.any(|item| item.code == "artifact_missing")
);
assert!(
diff.hard_failures
.iter()
.any(|item| item.code == "unsafe_control_class")
);
assert_eq!(report.status, SafetyStatusV1::Blocked);
assert_eq!(
report.next_actions,
vec!["resolve blocking deployment truth differences before mutation".to_string()]
);
}
#[test]
fn deployment_diff_warns_on_observation_gaps_without_blocking() {
let mut plan = sample_plan();
plan.expected_canisters.clear();
plan.role_artifacts[0].wasm_gz_sha256 = None;
plan.expected_verifier_readiness.required = false;
let inventory = DeploymentInventoryV1 {
schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
inventory_id: "inventory-1".to_string(),
observed_at: "2026-05-21T00:00:00Z".to_string(),
observed_identity: Some(sample_identity()),
local_config: LocalDeploymentConfigV1 {
config_path: Some("icp.yml".to_string()),
raw_config_sha256: None,
canonical_embedded_config_sha256: Some("runtime".to_string()),
},
observed_canisters: Vec::new(),
observed_pool: Vec::new(),
observed_artifacts: vec![ObservedArtifactV1 {
role: "root".to_string(),
artifact_path: "root.wasm.gz".to_string(),
file_sha256: Some("file".to_string()),
file_sha256_source: Some(ArtifactDigestSourceV1::ObservedFileDigest),
payload_sha256: None,
payload_size_bytes: Some(10),
source: ArtifactSourceV1::LocalBuild,
}],
observed_verifier_readiness: VerifierReadinessObservationV1 {
status: ObservationStatusV1::NotObserved,
role_epochs: Vec::new(),
},
unresolved_observations: vec![DeploymentObservationGapV1 {
key: "local_artifacts.user_hub".to_string(),
description: "missing built artifact".to_string(),
}],
};
let diff = compare_plan_to_inventory(&plan, &inventory);
let report = safety_report_from_diff("report-1", None, &diff);
assert_eq!(diff.resume_safety.status, SafetyStatusV1::Warning);
assert!(diff.hard_failures.is_empty());
assert!(
diff.artifact_diff
.iter()
.any(|item| item.category == "artifact_file_sha256"
&& item.severity == SafetySeverityV1::Info)
);
assert!(
diff.warnings
.iter()
.any(|item| item.code == "observation_gap")
);
assert_eq!(report.status, SafetyStatusV1::Warning);
}
#[test]
fn deployment_diff_is_safe_when_checked_facts_match() {
let mut plan = sample_plan();
plan.expected_verifier_readiness.required = false;
let inventory = DeploymentInventoryV1 {
schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
inventory_id: "inventory-1".to_string(),
observed_at: "2026-05-21T00:00:00Z".to_string(),
observed_identity: Some(sample_identity()),
local_config: LocalDeploymentConfigV1 {
config_path: Some("icp.yml".to_string()),
raw_config_sha256: None,
canonical_embedded_config_sha256: Some("runtime".to_string()),
},
observed_canisters: vec![ObservedCanisterV1 {
canister_id: "aaaaa-aa".to_string(),
role: Some("root".to_string()),
control_class: CanisterControlClassV1::DeploymentControlled,
controllers: vec!["aaaaa-aa".to_string()],
module_hash: None,
status: None,
root_trust_anchor: Some("aaaaa-aa".to_string()),
canonical_embedded_config_digest: None,
role_assignment_source: Some("local_install_state".to_string()),
}],
observed_pool: Vec::new(),
observed_artifacts: vec![ObservedArtifactV1 {
role: "root".to_string(),
artifact_path: "root.wasm.gz".to_string(),
file_sha256: Some("file".to_string()),
file_sha256_source: Some(ArtifactDigestSourceV1::ObservedFileDigest),
payload_sha256: Some("gzip".to_string()),
payload_size_bytes: Some(10),
source: ArtifactSourceV1::LocalBuild,
}],
observed_verifier_readiness: VerifierReadinessObservationV1 {
status: ObservationStatusV1::NotObserved,
role_epochs: Vec::new(),
},
unresolved_observations: Vec::new(),
};
let diff = compare_plan_to_inventory(&plan, &inventory);
let report = safety_report_from_diff("report-1", None, &diff);
assert_eq!(diff.resume_safety.status, SafetyStatusV1::Safe);
assert!(
diff.artifact_diff
.iter()
.any(|item| item.category == "artifact_file_sha256"
&& item.severity == SafetySeverityV1::Info)
);
assert!(diff.hard_failures.is_empty());
assert!(diff.warnings.is_empty());
assert_eq!(report.status, SafetyStatusV1::Safe);
assert!(report.next_actions.is_empty());
}
const SAMPLE_CONFIG: &str = r#"
controllers = []
app_index = []
[fleet]
name = "demo"
[app]
init_mode = "enabled"
[app.whitelist]
[subnets.prime.canisters.root]
kind = "root"
[subnets.prime.canisters.user_hub]
kind = "singleton"
"#;
fn assert_json_round_trip<T>(value: &T)
where
T: Clone + std::fmt::Debug + Eq + serde::de::DeserializeOwned + Serialize,
{
let encoded = serde_json::to_string(value).expect("value should encode");
let decoded = serde_json::from_str::<T>(&encoded).expect("value should decode");
assert_eq!(decoded, *value);
}
fn sample_identity() -> DeploymentIdentityV1 {
DeploymentIdentityV1 {
deployment_name: "local-root".to_string(),
network: "local".to_string(),
root_principal: Some("aaaaa-aa".to_string()),
authority_profile_hash: Some("authority".to_string()),
role_topology_hash: Some("topology".to_string()),
deployment_manifest_digest: Some("manifest".to_string()),
canonical_runtime_config_digest: Some("runtime".to_string()),
role_embedded_config_set_digest: Some("embedded".to_string()),
artifact_set_digest: Some("artifacts".to_string()),
pool_identity_set_digest: None,
canic_version: Some("0.41.0".to_string()),
ic_memory_version: Some("0.6.1".to_string()),
}
}
fn sample_role_artifact() -> RoleArtifactV1 {
RoleArtifactV1 {
role: "root".to_string(),
source: ArtifactSourceV1::LocalBuild,
build_profile: "fast".to_string(),
wasm_path: Some("root.wasm".to_string()),
wasm_gz_path: Some("root.wasm.gz".to_string()),
wasm_gz_size_bytes: Some(42),
wasm_sha256: Some("wasm".to_string()),
wasm_gz_sha256: Some("gzip".to_string()),
wasm_gz_sha256_source: Some(ArtifactDigestSourceV1::ReleaseSetManifest),
observed_wasm_gz_file_sha256: Some("file".to_string()),
observed_wasm_gz_file_sha256_source: Some(ArtifactDigestSourceV1::ObservedFileDigest),
installed_module_hash: Some("module".to_string()),
candid_path: Some("root.did".to_string()),
candid_sha256: Some("did".to_string()),
raw_config_sha256: Some("raw".to_string()),
canonical_embedded_config_sha256: Some("canonical".to_string()),
embedded_topology_sha256: Some("topology".to_string()),
builder_version: Some("0.41.0".to_string()),
rust_toolchain: Some("stable".to_string()),
package_version: Some("0.41.0".to_string()),
}
}
fn sample_plan() -> DeploymentPlanV1 {
DeploymentPlanV1 {
schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
plan_id: "plan-local-root".to_string(),
deployment_identity: sample_identity(),
trust_domain: TrustDomainV1 {
root_trust_anchor: Some("aaaaa-aa".to_string()),
migration_from: None,
},
fleet_template: "root".to_string(),
runtime_variant: "local".to_string(),
authority_profile: AuthorityProfileV1 {
profile_id: "local-default".to_string(),
expected_controllers: vec!["aaaaa-aa".to_string()],
staging_controllers: Vec::new(),
emergency_controllers: Vec::new(),
},
role_artifacts: vec![sample_role_artifact()],
expected_canisters: vec![ExpectedCanisterV1 {
role: "root".to_string(),
canister_id: Some("aaaaa-aa".to_string()),
control_class: CanisterControlClassV1::DeploymentControlled,
}],
expected_pool: Vec::new(),
expected_verifier_readiness: VerifierReadinessExpectationV1 {
required: true,
expected_role_epochs: vec![RoleEpochExpectationV1 {
role: "root".to_string(),
minimum_epoch: 1,
}],
},
unresolved_assumptions: Vec::new(),
}
}
struct TempWorkspace {
path: std::path::PathBuf,
}
impl TempWorkspace {
fn new(name: &str) -> Self {
let path = temp_dir(name);
fs::create_dir_all(&path).expect("create temp dir");
Self { path }
}
fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TempWorkspace {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
fn write_artifact(icp_root: &Path, role: &str, bytes: &[u8]) {
let path = icp_root
.join(".icp")
.join("local")
.join("canisters")
.join(role)
.join(format!("{role}.wasm.gz"));
fs::create_dir_all(path.parent().expect("artifact parent")).expect("create artifact dir");
fs::write(path, bytes).expect("write artifact");
}
fn write_release_set_manifest(icp_root: &Path) {
let path = icp_root
.join(".icp")
.join("local")
.join("canisters")
.join("root")
.join(ROOT_RELEASE_SET_MANIFEST_FILE);
let manifest = serde_json::json!({
"release_version": "0.41.1",
"entries": [{
"role": "user_hub",
"template_id": "embedded:user_hub",
"artifact_relative_path": ".icp/local/canisters/user_hub/user_hub.wasm.gz",
"payload_size_bytes": 17,
"payload_sha256_hex": "user-hub-hash",
"chunk_size_bytes": 1_048_576,
"chunk_sha256_hex": ["user-hub-hash"]
}]
});
fs::create_dir_all(path.parent().expect("manifest parent")).expect("create manifest dir");
fs::write(
path,
serde_json::to_vec_pretty(&manifest).expect("encode manifest"),
)
.expect("write manifest");
}