canic-host 0.70.6

Host-side build, install, deployment, and fleet-template library for Canic workspaces
Documentation
use super::*;

#[test]
fn install_truth_artifact_gate_blocks_materialized_digest_drift() {
    let root = temp_dir("canic-install-truth-artifact-digest-gate");
    let config_path = root.join("fleets/demo/canic.toml");
    fs::create_dir_all(config_path.parent().expect("config parent")).expect("create config dir");
    fs::write(
        &config_path,
        demo_config_source(
            r#"
[subnets.prime.canisters.root]
kind = "root"
"#,
        ),
    )
    .expect("write config");
    write_wasm_gz_artifact(&root, "root", b"root-artifact");

    let options = local_demo_install_options(&root);

    let mut check = current_install_deployment_truth_check_at(
        &options,
        &root,
        &root,
        &config_path,
        "demo",
        "2026-05-22T00:00:00Z".to_string(),
    )
    .expect("deployment truth check");
    check.plan.role_artifacts[0].observed_wasm_gz_file_sha256 =
        Some("different-observed-file-digest".to_string());
    check.diff = compare_plan_to_inventory(&check.plan, &check.inventory);
    check.report = safety_report_from_diff(
        "local:local:demo:report",
        Some("local:local:demo:diff".to_string()),
        &check.diff,
    );

    assert!(
        check
            .report
            .hard_failures
            .iter()
            .any(|finding| finding.code == "artifact_file_digest_mismatch")
    );
    assert!(enforce_install_deployment_truth_gate(&check).is_err());

    fs::remove_dir_all(root).expect("clean temp dir");
}

#[test]
fn install_truth_gate_blocks_observed_controller_drift() {
    let root = temp_dir("canic-install-truth-controller-gate");
    let config_path = root.join("fleets/demo/canic.toml");
    write_demo_root_only_config(&config_path);
    write_wasm_gz_artifact(&root, "root", b"root-artifact");

    let options = InstallRootOptions {
        root_canister: "root".to_string(),
        root_build_target: "root".to_string(),
        network: "local".to_string(),
        deployment_name: None,
        icp_root: Some(root.clone()),
        build_profile: Some(CanisterBuildProfile::Fast),
        ready_timeout_seconds: 30,
        config_path: Some("fleets/demo/canic.toml".to_string()),
        expected_fleet: Some("demo".to_string()),
        interactive_config_selection: false,
        deployment_plan_override: None,
        artifact_promotion_plan_override: None,
    };

    let mut check = current_install_deployment_truth_check_at(
        &options,
        &root,
        &root,
        &config_path,
        "demo",
        "2026-05-22T00:00:00Z".to_string(),
    )
    .expect("deployment truth check");
    check.plan.authority_profile.expected_controllers = vec!["aaaaa-aa".to_string()];
    check.inventory.observed_canisters = vec![ObservedCanisterV1 {
        canister_id: "aaaaa-aa".to_string(),
        role: Some("root".to_string()),
        control_class: CanisterControlClassV1::DeploymentControlled,
        controllers: vec!["external-controller".to_string()],
        module_hash: None,
        status: Some("running".to_string()),
        root_trust_anchor: Some("aaaaa-aa".to_string()),
        canonical_embedded_config_digest: None,
        role_assignment_source: Some("icp_canister_status".to_string()),
    }];
    check.diff = compare_plan_to_inventory(&check.plan, &check.inventory);
    check.report = safety_report_from_diff(
        "local:local:demo:report",
        Some("local:local:demo:diff".to_string()),
        &check.diff,
    );

    assert!(
        check
            .report
            .hard_failures
            .iter()
            .any(|finding| finding.code == "expected_controller_missing")
    );
    assert!(enforce_install_deployment_truth_gate(&check).is_err());
    let receipt = install_deployment_truth_gate_receipt(
        &check,
        "start".to_string(),
        vec![artifact_gate_phase_receipt(
            &check,
            "start",
            Some("finish".into()),
        )],
        artifact_gate_role_phase_receipts(&check),
    );
    let lines = install_deployment_truth_gate_lines(&check, &receipt);
    assert!(
        lines
            .iter()
            .any(|line| line.contains("Deployment truth blocker: diff:expected_controller_missing"))
    );
    assert!(lines.iter().any(|line| {
        line.contains("Deployment truth receipt:") && line.contains("status=FailedBeforeMutation")
    }));
    let err = enforce_install_deployment_truth_gate(&check).unwrap_err();
    assert!(
        err.to_string()
            .contains("diff:expected_controller_missing:"),
        "unexpected error: {err}"
    );

    fs::remove_dir_all(root).expect("clean temp dir");
}

#[test]
fn install_truth_gate_blocks_missing_expected_root_canister() {
    let root = temp_dir("canic-install-truth-missing-root-gate");
    let config_path = root.join("fleets/demo/canic.toml");
    fs::create_dir_all(config_path.parent().expect("config parent")).expect("create config dir");
    fs::write(
        &config_path,
        demo_config_source(
            r#"
[subnets.prime.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(),
        network: "local".to_string(),
        deployment_name: None,
        icp_root: Some(root.clone()),
        build_profile: Some(CanisterBuildProfile::Fast),
        ready_timeout_seconds: 30,
        config_path: Some("fleets/demo/canic.toml".to_string()),
        expected_fleet: Some("demo".to_string()),
        interactive_config_selection: false,
        deployment_plan_override: None,
        artifact_promotion_plan_override: None,
    };

    let mut check = current_install_deployment_truth_check_at(
        &options,
        &root,
        &root,
        &config_path,
        "demo",
        "2026-05-22T00:00:00Z".to_string(),
    )
    .expect("deployment truth check");
    check.plan.expected_canisters[0].canister_id = Some("aaaaa-aa".to_string());
    check.inventory.observed_canisters = vec![ObservedCanisterV1 {
        canister_id: "different-root".to_string(),
        role: Some("root".to_string()),
        control_class: CanisterControlClassV1::DeploymentControlled,
        controllers: vec!["aaaaa-aa".to_string()],
        module_hash: None,
        status: Some("running".to_string()),
        root_trust_anchor: Some("different-root".to_string()),
        canonical_embedded_config_digest: None,
        role_assignment_source: Some("icp_canister_status".to_string()),
    }];
    check.diff = compare_plan_to_inventory(&check.plan, &check.inventory);
    check.report = safety_report_from_diff(
        "local:local:demo:report",
        Some("local:local:demo:diff".to_string()),
        &check.diff,
    );

    assert!(
        check
            .report
            .hard_failures
            .iter()
            .any(|finding| finding.code == "canister_missing")
    );
    let err = enforce_install_deployment_truth_gate(&check).unwrap_err();
    assert!(
        err.to_string().contains("canister_missing:"),
        "unexpected error: {err}"
    );

    fs::remove_dir_all(root).expect("clean temp dir");
}

#[test]
fn install_truth_gate_blocks_all_safety_report_hard_failures() {
    let root = temp_dir("canic-install-truth-all-hard-failures");
    let config_path = root.join("fleets/demo/canic.toml");
    fs::create_dir_all(config_path.parent().expect("config parent")).expect("create config dir");
    fs::write(
        &config_path,
        demo_config_source(
            r#"
[subnets.prime.canisters.root]
kind = "root"
"#,
        ),
    )
    .expect("write config");
    write_wasm_gz_artifact(&root, "root", b"root-artifact");

    let options = local_demo_install_options(&root);

    let mut check = current_install_deployment_truth_check_at(
        &options,
        &root,
        &root,
        &config_path,
        "demo",
        "2026-05-22T00:00:00Z".to_string(),
    )
    .expect("deployment truth check");
    check.report.hard_failures.push(SafetyFindingV1 {
        code: "future_hard_failure".to_string(),
        message: "future deployment truth blocker".to_string(),
        severity: SafetySeverityV1::HardFailure,
        subject: Some("future.subject".to_string()),
    });

    let err = enforce_install_deployment_truth_gate(&check).unwrap_err();

    assert!(
        err.to_string().contains("future_hard_failure:"),
        "unexpected error: {err}"
    );

    fs::remove_dir_all(root).expect("clean temp dir");
}