canic-host 0.99.23

Host-side App build, Fleet install, deployment, and release-set library for Canic workspaces
Documentation
use super::build_network::resolve_install_build_context;
use super::build_snapshot::InstallBuildTarget;
use super::commands::{
    add_create_root_target, add_icp_environment_target, icp_canister_command, root_init_args,
};
use super::config_selection::{
    config_selection_error, discover_canic_config_choices, discover_project_canic_config_choices,
    resolve_install_config_path, select_discovered_app_config_path,
};
use super::current_execution::{
    current_install_execution_context, current_install_executor_missing_capabilities,
};
use super::deployment_truth_gate::{
    enforce_install_deployment_truth_gate, install_deployment_truth_gate_lines,
    install_deployment_truth_gate_receipt,
};
use super::execution_preflight::write_current_install_execution_preflight_receipt;
use super::operations::{
    BuildInstallTargetsOperation, EmitRootManifestOperation, InstallPhaseLabel,
    InstallPhaseOperation, InstallRootWasmOperation, ResolveRootCanisterOperation,
};
use super::output::render_install_timing_summary;
use super::phase_receipts::{
    CompletedInstallPhase, InstallReceiptScope, install_deployment_truth_phase_receipt,
    write_completed_install_phase_receipt,
};
use super::plan_artifacts::{
    PlanArtifactError, PreparedPlanArtifacts, prepare_plan_artifacts_with_phase,
};
use super::receipt_io::{
    install_deployment_truth_receipt_path, write_install_deployment_truth_receipt,
};
use super::root_cycles::add_local_root_create_cycles_arg;
use super::timing::InstallTimingSummary;
use super::truth_check::current_install_deployment_truth_check_at;
use super::{
    FleetActivationContinuationRequired, InstallRootBlockKind, InstallRootBlockedError,
    InstallRootError, InstallRootOptions, InstallRootPhase, check_install_deployment_truth,
    check_install_execution_preflight, latest_deployment_truth_receipt_path_from_root,
};
use crate::canister_build::{
    CanisterArtifactBuildSpec, CanisterBuildProfile, WorkspaceBuildContext,
};
use crate::deployment_truth::{
    CanisterControlClassV1, DeploymentCheckV1, DeploymentExecutionContextV1,
    DeploymentExecutionPreflightStatusV1, DeploymentExecutionStatusV1, DeploymentExecutorBackendV1,
    DeploymentExecutorCapabilityV1, DeploymentReceiptV1, ObservationStatusV1, ObservedCanisterV1,
    SafetyFindingV1, SafetySeverityV1, SafetyStatusV1, artifact_gate_phase_receipt,
    artifact_gate_role_phase_receipts, compare_plan_to_inventory, safety_report_from_diff,
};
use crate::icp::LocalReplicaTarget;
use crate::release_set::RootReleaseSetBuildSnapshot;
use crate::test_support::{temp_dir, write_local_network_authority};
use canic_core::{
    dto::fleet_activation::FleetActivationIdentity,
    ids::{
        AppId, BuildNetwork, CanonicalNetworkId, FleetBinding, FleetId, FleetKey, ReleaseBuildId,
        ReleaseBuildNonce,
    },
};
use std::{
    fs,
    path::{Path, PathBuf},
    time::Duration,
};

mod commands;
mod config_selection;
mod install_truth;

#[test]
fn public_install_error_preserves_phase_and_typed_source() {
    let error = InstallRootError::new(
        InstallRootPhase::Activation,
        std::io::Error::other("activation failed"),
    );

    assert_eq!(error.phase(), InstallRootPhase::Activation);
    assert!(
        std::error::Error::source(&error)
            .and_then(|source| source.downcast_ref::<std::io::Error>())
            .is_some()
    );
}

#[test]
fn activation_continuation_exposes_typed_recovery_identity() {
    let continuation = FleetActivationContinuationRequired {
        root_canister_id: "uxrrr-q7777-77774-qaaaq-cai".to_string(),
        journal_path: PathBuf::from("/tmp/fleet-activation.json"),
        sequence: 3,
    };

    assert_eq!(
        continuation.root_canister_id(),
        "uxrrr-q7777-77774-qaaaq-cai"
    );
    assert_eq!(
        continuation.journal_path(),
        Path::new("/tmp/fleet-activation.json")
    );
    assert_eq!(continuation.sequence(), 3);
}

#[test]
fn named_ic_environment_is_explicit_for_cargo_builds() {
    let root = temp_dir("canic-install-build-environment");
    fs::create_dir_all(&root).expect("create root");
    fs::write(
        root.join("icp.yaml"),
        "environments:\n  - name: staging\n    network: ic\n",
    )
    .expect("write icp yaml");

    let context = resolve_install_build_context(
        &root,
        &root,
        &root.join("canic.toml"),
        "staging",
        "root",
        Some(CanisterBuildProfile::Fast),
    )
    .expect("resolve build context");
    let mut command = std::process::Command::new("cargo");
    context.apply_to_command(&mut command);

    assert_eq!(context.environment, "staging");
    assert_eq!(context.build_network, BuildNetwork::Ic);
    assert!(command.get_envs().any(|(key, value)| {
        key == "ICP_ENVIRONMENT" && value.is_some_and(|value| value == "ic")
    }));
    fs::remove_dir_all(root).expect("remove temp root");
}

fn source_section<'a>(source: &'a str, start: &str, end: &str) -> &'a str {
    let start_index = source.find(start).expect("source section start exists");
    let end_index = source[start_index..]
        .find(end)
        .map(|offset| start_index + offset)
        .expect("source section end exists");
    &source[start_index..end_index]
}

fn assert_before(source: &str, before: &str, after: &str) {
    let before_index = source.find(before).expect("before marker exists");
    let after_index = source.find(after).expect("after marker exists");
    assert!(
        before_index < after_index,
        "`{before}` must appear before `{after}`"
    );
}

fn write_temp_workspace_config(config_source: &str) -> PathBuf {
    let root = temp_dir("canic-install-test");
    fs::create_dir_all(root.join("apps")).expect("temp apps dir must be created");
    fs::write(root.join("apps/canic.toml"), config_source)
        .expect("temp canic.toml must be written");
    root
}

fn demo_config_source(attached: &str) -> String {
    format!(
        r#"
controllers = []
[services.fleet]
roles = []

[app]
name = "demo"
init_mode = "enabled"


[roles.root]
kind = "root"
package = "root"

[roles.app]
kind = "canister"
package = "app"

[roles.project_registry]
kind = "canister"
package = "project_registry"

[roles.oracle_pokemon]
kind = "canister"
package = "oracle_pokemon"

[roles.user_hub]
kind = "canister"
package = "user_hub"

[roles.user_shard]
kind = "canister"
package = "user_shard"

[roles.scale_hub]
kind = "canister"
package = "scale_hub"

[roles.scale_replica]
kind = "canister"
package = "scale"

[roles.worker]
kind = "canister"
package = "worker"
[app.whitelist]

{attached}
"#
    )
}

fn local_demo_install_options(root: &Path) -> InstallRootOptions {
    write_local_network_authority(root, "local");
    InstallRootOptions {
        root_canister: "root".to_string(),
        root_build_target: "root".to_string(),
        environment: "local".to_string(),
        fleet_name: "demo".to_string(),
        icp_root: Some(root.to_path_buf()),
        build_profile: Some(CanisterBuildProfile::Fast),
        config_path: Some("apps/demo/canic.toml".to_string()),
        expected_app: Some("demo".to_string()),
        interactive_config_selection: false,
        deployment_plan_override: None,
    }
}

fn write_demo_root_only_config(config_path: &Path) {
    fs::create_dir_all(config_path.parent().expect("config parent")).expect("create config dir");
    fs::write(
        config_path,
        r#"
controllers = []
[services.fleet]
roles = []

[app]
name = "demo"
init_mode = "enabled"


[roles.root]
kind = "root"
package = "root"
[app.whitelist]

[subnets.default.canisters.root]
kind = "root"
"#,
    )
    .expect("write config");
}

fn write_wasm_gz_artifact(root: &Path, role: &str, bytes: &[u8]) {
    let path = root
        .join(".icp/local/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 demo_install_deployment_truth_check(root_name: &str) -> (PathBuf, DeploymentCheckV1) {
    let root = temp_dir(root_name);
    write_local_network_authority(&root, "local");
    let config_path = root.join("apps/demo/canic.toml");
    fs::create_dir_all(config_path.parent().expect("config parent")).expect("create config dir");
    fs::write(
        &config_path,
        r#"
controllers = []
[services.fleet]
roles = []

[app]
name = "demo"
init_mode = "enabled"


[roles.root]
kind = "root"
package = "root"

[roles.app]
kind = "canister"
package = "app"

[roles.project_registry]
kind = "canister"
package = "project_registry"

[roles.oracle_pokemon]
kind = "canister"
package = "oracle_pokemon"

[roles.user_hub]
kind = "canister"
package = "user_hub"

[roles.user_shard]
kind = "canister"
package = "user_shard"

[roles.scale_hub]
kind = "canister"
package = "scale_hub"

[roles.scale_replica]
kind = "canister"
package = "scale"

[roles.role_baseline]
kind = "canister"
package = "role_baseline"

[roles.worker]
kind = "canister"
package = "worker"
[app.whitelist]

[subnets.default.canisters.root]
kind = "root"
"#,
    )
    .expect("write config");
    write_wasm_gz_artifact(&root, "root", b"root-artifact");

    let options = InstallRootOptions {
        root_canister: "root".to_string(),
        root_build_target: "root".to_string(),
        environment: "local".to_string(),
        fleet_name: "demo".to_string(),
        icp_root: Some(root.clone()),
        build_profile: Some(CanisterBuildProfile::Fast),
        config_path: Some("apps/demo/canic.toml".to_string()),
        expected_app: Some("demo".to_string()),
        interactive_config_selection: false,
        deployment_plan_override: None,
    };
    let check = current_install_deployment_truth_check_at(
        &options,
        &root,
        &root,
        &config_path,
        "demo",
        "2026-05-22T00:00:00Z".to_string(),
    )
    .expect("deployment truth check");
    (root, check)
}

fn sample_fleet_activation_identity() -> FleetActivationIdentity {
    FleetActivationIdentity {
        fleet: FleetBinding {
            fleet: FleetKey {
                network: CanonicalNetworkId::public_ic(),
                fleet_id: FleetId::from_generated_bytes([7; 32]),
            },
            app: AppId::from("demo"),
        },
        operation_id: [8; 32],
        release_build_id: ReleaseBuildId::from_nonce(ReleaseBuildNonce::from_random_bytes([9; 32])),
    }
}