canic-host 0.70.5

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

#[test]
fn deployment_execution_preflight_accepts_safe_plan_and_capable_executor() {
    let check = sample_check(sample_plan(), sample_matching_inventory());
    let authority = build_authority_reconciliation_plan(&check);
    let executor = CurrentCliDeploymentExecutor::new(
        Some("/workspace/canic".to_string()),
        Some("/workspace/canic/.icp".to_string()),
        vec!["/workspace/canic/.icp/local/canisters".to_string()],
    );

    let preflight = deployment_execution_preflight(
        &check.plan,
        &check.report,
        &authority,
        &executor,
        CURRENT_CLI_EXECUTOR_CAPABILITIES,
    );

    assert_eq!(preflight.plan_id, check.plan.plan_id);
    assert_eq!(preflight.safety_report_id, check.report.report_id);
    assert_eq!(preflight.authority_plan_id, authority.plan_id);
    assert_eq!(
        preflight.status,
        DeploymentExecutionPreflightStatusV1::Ready
    );
    assert!(preflight.blockers.is_empty());
    assert!(preflight.missing_capabilities.is_empty());
    assert_eq!(
        preflight.planned_phases,
        vec![
            "resolve_root_canister",
            "build_artifacts",
            "materialize_artifacts",
            "execution_preflight",
            "emit_manifest",
            "install_root",
            "fund_root_pre_bootstrap",
            "stage_release_set",
            "resume_bootstrap",
            "wait_ready",
            "fund_root_post_ready",
            "write_install_state",
        ]
    );
    assert_json_round_trip(&preflight);
}

#[test]
fn deployment_execution_preflight_from_check_derives_authority_plan() {
    let check = sample_check(sample_plan(), sample_matching_inventory());
    let executor = CurrentCliDeploymentExecutor::new(
        Some("/workspace/canic".to_string()),
        Some("/workspace/canic/.icp".to_string()),
        vec!["/workspace/canic/.icp/local/canisters".to_string()],
    );

    let from_check = deployment_execution_preflight_from_check(
        &check,
        &executor,
        CURRENT_CLI_EXECUTOR_CAPABILITIES,
    );
    let authority = build_authority_reconciliation_plan(&check);
    let explicit = deployment_execution_preflight(
        &check.plan,
        &check.report,
        &authority,
        &executor,
        CURRENT_CLI_EXECUTOR_CAPABILITIES,
    );

    assert_eq!(from_check, explicit);
}

#[test]
fn deployment_execution_preflight_validation_accepts_check_derived_artifact() {
    let check = sample_check(sample_plan(), sample_matching_inventory());
    let executor = CurrentCliDeploymentExecutor::new(
        Some("/workspace/canic".to_string()),
        Some("/workspace/canic/.icp".to_string()),
        vec!["/workspace/canic/.icp/local/canisters".to_string()],
    );
    let preflight = deployment_execution_preflight_from_check(
        &check,
        &executor,
        CURRENT_CLI_EXECUTOR_CAPABILITIES,
    );

    validate_deployment_execution_preflight(&preflight).expect("preflight should validate");
    validate_deployment_execution_preflight_for_check(&check, &preflight)
        .expect("preflight should match source check");
}

#[test]
fn deployment_execution_preflight_validation_rejects_mutated_status() {
    let check = sample_unknown_unsafe_check();
    let executor = CurrentCliDeploymentExecutor::new(
        Some("/workspace/canic".to_string()),
        Some("/workspace/canic/.icp".to_string()),
        vec!["/workspace/canic/.icp/local/canisters".to_string()],
    );
    let mut preflight = deployment_execution_preflight_from_check(
        &check,
        &executor,
        CURRENT_CLI_EXECUTOR_CAPABILITIES,
    );
    preflight.status = DeploymentExecutionPreflightStatusV1::Ready;

    let err = validate_deployment_execution_preflight(&preflight)
        .expect_err("ready status with blockers should fail");

    std::assert_matches!(
        err,
        DeploymentExecutionPreflightError::StatusBlockerMismatch { .. }
    );
}

#[test]
fn deployment_execution_preflight_validation_rejects_source_check_mismatch() {
    let check = sample_check(sample_plan(), sample_matching_inventory());
    let executor = CurrentCliDeploymentExecutor::new(
        Some("/workspace/canic".to_string()),
        Some("/workspace/canic/.icp".to_string()),
        vec!["/workspace/canic/.icp/local/canisters".to_string()],
    );
    let mut preflight = deployment_execution_preflight_from_check(
        &check,
        &executor,
        CURRENT_CLI_EXECUTOR_CAPABILITIES,
    );
    preflight.plan_id = "other-plan".to_string();

    let err = validate_deployment_execution_preflight_for_check(&check, &preflight)
        .expect_err("preflight from another plan should fail");

    std::assert_matches!(
        err,
        DeploymentExecutionPreflightError::SourceCheckMismatch {
            field: "plan_id",
            ..
        }
    );
}

#[test]
fn deployment_execution_preflight_validation_rejects_capability_inconsistency() {
    let check = sample_unknown_unsafe_check();
    let executor = LimitedExecutor {
        context: DeploymentExecutionContextV1 {
            workspace_root: Some("/workspace/canic".to_string()),
            icp_root: Some("/workspace/canic/.icp".to_string()),
            artifact_roots: Vec::new(),
            backend: DeploymentExecutorBackendV1::Other {
                name: "limited-test-backend".to_string(),
            },
            backend_capabilities: vec![DeploymentExecutorCapabilityV1::CanisterStatus],
        },
    };
    let mut preflight = deployment_execution_preflight_from_check(
        &check,
        &executor,
        &[DeploymentExecutorCapabilityV1::CanisterStatus],
    );
    preflight
        .missing_capabilities
        .push(DeploymentExecutorCapabilityV1::InstallCode);

    let err = validate_deployment_execution_preflight(&preflight)
        .expect_err("missing non-required capability should fail");

    std::assert_matches!(
        err,
        DeploymentExecutionPreflightError::MissingCapabilityNotRequired {
            capability: DeploymentExecutorCapabilityV1::InstallCode
        }
    );
}

#[test]
fn deployment_execution_preflight_v1_json_schema_shape_is_stable() {
    let check = sample_unknown_unsafe_check();
    let executor = LimitedExecutor {
        context: DeploymentExecutionContextV1 {
            workspace_root: Some("/workspace/canic".to_string()),
            icp_root: Some("/workspace/canic/.icp".to_string()),
            artifact_roots: Vec::new(),
            backend: DeploymentExecutorBackendV1::Other {
                name: "limited-test-backend".to_string(),
            },
            backend_capabilities: vec![DeploymentExecutorCapabilityV1::CanisterStatus],
        },
    };
    let preflight = deployment_execution_preflight_from_check(
        &check,
        &executor,
        &[
            DeploymentExecutorCapabilityV1::CanisterStatus,
            DeploymentExecutorCapabilityV1::StageArtifact,
        ],
    );
    let value = serde_json::to_value(&preflight).expect("encode execution preflight");

    assert_object_keys(
        &value,
        &[
            "schema_version",
            "plan_id",
            "safety_report_id",
            "authority_plan_id",
            "backend",
            "status",
            "planned_phases",
            "required_capabilities",
            "missing_capabilities",
            "blockers",
        ],
    );
    assert_eq!(value["schema_version"], DEPLOYMENT_TRUTH_SCHEMA_VERSION);
    assert_eq!(value["plan_id"], "plan-local-root");
    assert_eq!(value["safety_report_id"], "report-1");
    assert_eq!(value["authority_plan_id"], "plan-local-root");
    assert_eq!(value["backend"]["Other"]["name"], "limited-test-backend");
    assert_eq!(value["status"], "Blocked");
    assert_eq!(value["required_capabilities"][0], "CanisterStatus");
    assert_eq!(value["required_capabilities"][1], "StageArtifact");
    assert_eq!(value["missing_capabilities"][0], "StageArtifact");
    assert_eq!(
        value["blockers"]
            .as_array()
            .expect("blockers should be array")
            .iter()
            .filter(|finding| finding["code"] == "executor_capability_missing")
            .count(),
        1
    );
}

#[test]
fn deployment_execution_preflight_text_reports_passive_readiness() {
    let check = sample_check(sample_plan(), sample_matching_inventory());
    let executor = CurrentCliDeploymentExecutor::new(
        Some("/workspace/canic".to_string()),
        Some("/workspace/canic/.icp".to_string()),
        vec!["/workspace/canic/.icp/local/canisters".to_string()],
    );
    let preflight = deployment_execution_preflight_from_check(
        &check,
        &executor,
        CURRENT_CLI_EXECUTOR_CAPABILITIES,
    );

    let text = deployment_execution_preflight_text(&preflight);

    assert!(text.contains("Deployment execution preflight"));
    assert!(text.contains("mode: passive"));
    assert!(text.contains("status: ready"));
    assert!(text.contains("plan_id: plan-local-root"));
    assert!(text.contains("backend: CurrentCli"));
    assert!(text.contains("planned_phases:"));
    assert!(text.contains("  - install_root"));
    assert!(text.contains("required_capabilities:"));
    assert!(text.contains("  - StageArtifact"));
}

#[test]
fn deployment_execution_preflight_blocks_safety_authority_and_capability_gaps() {
    let mut check = sample_unknown_unsafe_check();
    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 authority = build_authority_reconciliation_plan(&check);
    let executor = LimitedExecutor {
        context: DeploymentExecutionContextV1 {
            workspace_root: Some("/workspace/canic".to_string()),
            icp_root: Some("/workspace/canic/.icp".to_string()),
            artifact_roots: Vec::new(),
            backend: DeploymentExecutorBackendV1::Other {
                name: "limited-test-backend".to_string(),
            },
            backend_capabilities: vec![DeploymentExecutorCapabilityV1::CanisterStatus],
        },
    };

    let preflight = deployment_execution_preflight(
        &check.plan,
        &check.report,
        &authority,
        &executor,
        &[
            DeploymentExecutorCapabilityV1::CanisterStatus,
            DeploymentExecutorCapabilityV1::StageArtifact,
        ],
    );

    assert_eq!(
        preflight.status,
        DeploymentExecutionPreflightStatusV1::Blocked
    );
    assert_eq!(
        preflight.missing_capabilities,
        vec![DeploymentExecutorCapabilityV1::StageArtifact]
    );
    assert!(preflight.blockers.iter().any(|finding| {
        finding.code == "deployment_safety_blocked"
            && finding.subject.as_deref() == Some("report-1")
    }));
    assert!(
        preflight
            .blockers
            .iter()
            .any(|finding| finding.code == "deployment_artifact_missing")
    );
    assert!(
        preflight
            .blockers
            .iter()
            .any(|finding| finding.code == "authority_unsafe_blocked")
    );
    assert!(preflight.blockers.iter().any(|finding| {
        finding.code == "executor_capability_missing"
            && finding.subject.as_deref() == Some("StageArtifact")
    }));
}