canic-host 0.92.6

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() {
    let root = temp_dir("canic-install-truth-preflight");
    fs::create_dir_all(&root).expect("create project root");
    fs::write(root.join("Cargo.toml"), "[workspace]\nmembers = []\n")
        .expect("write workspace manifest");
    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 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());

    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");

    let blocked = err
        .downcast_ref::<InstallRootBlockedError>()
        .expect("blocked preflight should retain its typed reason");
    assert_eq!(
        blocked.kind(),
        InstallRootBlockKind::DeploymentExecutionPreflight
    );
    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,
        ],
    );
}