canic-host 0.69.2

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

#[test]
fn install_truth_preflight_uses_current_install_inputs_without_mutation() {
    with_guarded_env(|| {
        let root = temp_dir("canic-install-truth-preflight");
        fs::create_dir_all(root.join("fleets/demo")).expect("create config dir");
        fs::write(
            root.join("fleets/demo/canic.toml"),
            demo_config_source(
                r#"
[subnets.prime.canisters.root]
kind = "root"

[subnets.prime.canisters.user_hub]
kind = "service"
"#,
            ),
        )
        .expect("write config");
        write_wasm_gz_artifact(&root, "root", b"root-artifact");
        write_wasm_gz_artifact(&root, "wasm_store", b"wasm-store-artifact");
        write_wasm_gz_artifact(&root, "user_hub", b"user-hub-artifact");
        let previous_workspace_root = env::var_os("CANIC_WORKSPACE_ROOT");
        unsafe {
            env::set_var("CANIC_WORKSPACE_ROOT", &root);
        }

        let options = local_demo_install_options(&root);

        let check = check_install_deployment_truth(&options, "2026-05-22T00:00:00Z")
            .expect("install truth preflight");
        let execution_preflight =
            check_install_execution_preflight(&options, "2026-05-22T00:00:01Z")
                .expect("install execution preflight");

        assert_eq!(check.check_id, "local:local:demo:check");
        assert_eq!(check.plan.fleet_template, "demo");
        assert_eq!(
            check
                .plan
                .role_artifacts
                .iter()
                .map(|artifact| artifact.build_profile.as_str())
                .collect::<Vec<_>>(),
            vec!["fast", "fast", "fast"]
        );
        assert_eq!(check.inventory.observed_artifacts.len(), 3);
        enforce_install_deployment_truth_gate(&check)
            .expect("complete local artifacts should pass gate");
        assert_eq!(execution_preflight.plan_id, check.plan.plan_id);
        assert_eq!(
            execution_preflight.backend,
            DeploymentExecutorBackendV1::CurrentCli
        );
        assert!(execution_preflight.missing_capabilities.is_empty());
        assert_eq!(
            execution_preflight.status,
            DeploymentExecutionPreflightStatusV1::Ready
        );
        assert!(execution_preflight.blockers.is_empty());
        assert!(!root.join(".canic").exists());

        restore_env_var("CANIC_WORKSPACE_ROOT", previous_workspace_root);
        fs::remove_dir_all(root).expect("clean temp dir");
    });
}

#[test]
fn install_truth_execution_preflight_receipt_records_ready_state() {
    let (root, mut check) =
        demo_install_deployment_truth_check("canic-install-truth-execution-preflight-ready");
    check.plan.expected_canisters.clear();
    check.report.status = SafetyStatusV1::Safe;
    check.report.summary = "deployment inventory matches plan".to_string();
    check.report.hard_failures.clear();
    let execution_context = current_install_execution_context(&root, &root, "local");

    let path = write_current_install_execution_preflight_receipt(
        &root,
        "local",
        "demo",
        &check,
        &execution_context,
    )
    .expect("write execution preflight receipt");
    let receipt: DeploymentReceiptV1 =
        serde_json::from_slice(&fs::read(path).expect("read receipt")).expect("decode receipt");

    assert_eq!(
        receipt.operation_id,
        "local:local:demo:check:execution_preflight"
    );
    assert_eq!(
        receipt.operation_status,
        DeploymentExecutionStatusV1::Complete
    );
    assert_eq!(receipt.phase_receipts[0].phase, "execution_preflight");
    assert!(
        receipt.phase_receipts[0]
            .verified_postcondition
            .evidence
            .contains(&"execution_preflight_status:Ready".to_string())
    );
    assert!(
        receipt.phase_receipts[0]
            .verified_postcondition
            .evidence
            .contains(&"blockers:0".to_string())
    );
    assert!(receipt.execution_context.is_some());

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

#[test]
fn install_truth_execution_preflight_receipt_records_blocked_state_before_error() {
    let (root, mut check) =
        demo_install_deployment_truth_check("canic-install-truth-execution-preflight-blocked");
    check.report.status = SafetyStatusV1::Blocked;
    check.report.hard_failures.push(SafetyFindingV1 {
        code: "deployment_artifact_missing".to_string(),
        message: "planned artifact was not observed".to_string(),
        severity: SafetySeverityV1::HardFailure,
        subject: Some("root".to_string()),
    });
    let execution_context = current_install_execution_context(&root, &root, "local");

    let err = write_current_install_execution_preflight_receipt(
        &root,
        "local",
        "demo",
        &check,
        &execution_context,
    )
    .expect_err("blocked execution preflight should stop install");

    assert!(
        err.to_string()
            .contains("deployment execution preflight blocked install")
    );
    let path = latest_deployment_truth_receipt_path_from_root(&root, "local", "demo")
        .expect("find latest receipt")
        .expect("blocked preflight receipt should be written");
    let receipt: DeploymentReceiptV1 =
        serde_json::from_slice(&fs::read(path).expect("read receipt")).expect("decode receipt");
    assert_eq!(
        receipt.operation_status,
        DeploymentExecutionStatusV1::FailedBeforeMutation
    );
    assert!(
        receipt.phase_receipts[0]
            .verified_postcondition
            .evidence
            .contains(&"execution_preflight_status:Blocked".to_string())
    );
    assert!(
        receipt.phase_receipts[0]
            .verified_postcondition
            .evidence
            .iter()
            .any(|line| line.starts_with("blocker:deployment_artifact_missing:"))
    );

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

#[test]
fn install_truth_reports_executor_missing_required_capabilities() {
    let context = DeploymentExecutionContextV1 {
        workspace_root: Some("/workspace/canic".to_string()),
        icp_root: Some("/workspace/canic/.icp".to_string()),
        artifact_roots: vec!["/workspace/canic/.icp/local/canisters".to_string()],
        backend: DeploymentExecutorBackendV1::Other {
            name: "limited-test-backend".to_string(),
        },
        backend_capabilities: vec![DeploymentExecutorCapabilityV1::CanisterStatus],
    };

    assert_eq!(
        current_install_executor_missing_capabilities(&context),
        vec![
            DeploymentExecutorCapabilityV1::CreateCanister,
            DeploymentExecutorCapabilityV1::InstallCode,
            DeploymentExecutorCapabilityV1::Call,
            DeploymentExecutorCapabilityV1::Query,
            DeploymentExecutorCapabilityV1::StageArtifact,
        ],
    );
}