canic-host 0.68.22

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

#[test]
fn deployment_comparison_report_detects_cross_deployment_drift() {
    let left = sample_check(sample_plan(), sample_matching_inventory());
    let mut right_plan = sample_plan();
    right_plan.plan_id = "plan-prod-root".to_string();
    right_plan.deployment_identity.deployment_name = "prod".to_string();
    right_plan.deployment_identity.network = "ic".to_string();
    right_plan.trust_domain.root_trust_anchor = Some("prod-root".to_string());
    right_plan.role_artifacts[0].wasm_sha256 = Some(sample_sha256("b"));
    let mut right_inventory = sample_matching_inventory();
    right_inventory.inventory_id = "inventory-prod".to_string();
    right_inventory.observed_canisters[0].control_class = CanisterControlClassV1::UserControlled;
    right_inventory.observed_canisters[0].controllers = vec!["user-principal".to_string()];
    right_inventory.observed_canisters[0].module_hash = Some("prod-module".to_string());
    right_inventory.observed_canisters[0].canonical_embedded_config_digest =
        Some("prod-config".to_string());
    let mut right = sample_check(right_plan, right_inventory);
    right.check_id = "check-prod".to_string();

    let report = deployment_comparison_report_from_checks(
        "comparison-1",
        "2026-05-26T00:00:00Z",
        "staging",
        "prod",
        &left,
        &right,
    );

    assert_eq!(report.schema_version, DEPLOYMENT_TRUTH_SCHEMA_VERSION);
    assert_eq!(report.report_id, "comparison-1");
    assert_eq!(report.report_digest.len(), 64);
    assert_eq!(report.left.label, "staging");
    assert_eq!(report.right.label, "prod");
    assert_eq!(report.status, SafetyStatusV1::Blocked);
    assert!(!report.identity_diff.is_empty());
    assert!(!report.artifact_diff.is_empty());
    assert!(!report.module_hash_diff.is_empty());
    assert!(!report.embedded_config_diff.is_empty());
    assert!(!report.authority_diff.is_empty());
    assert!(!report.external_lifecycle_diff.is_empty());
    assert!(
        report
            .hard_failures
            .iter()
            .any(|failure| failure.code == "deployment_comparison_input_blocked")
    );
    assert!(
        report
            .warnings
            .iter()
            .any(|warning| warning.code == "deployment_comparison_drift")
    );
    validate_deployment_comparison_report(&report).expect("comparison should validate");
}

#[test]
fn deployment_comparison_report_validation_rejects_digest_drift() {
    let left = sample_check(sample_plan(), sample_matching_inventory());
    let right = sample_check(sample_plan(), sample_matching_inventory());
    let mut report = deployment_comparison_report_from_checks(
        "comparison-1",
        "2026-05-26T00:00:00Z",
        "left",
        "right",
        &left,
        &right,
    );

    report.next_actions.push("stale action".to_string());

    let err = validate_deployment_comparison_report(&report)
        .expect_err("stale comparison digest should fail");
    assert_eq!(
        err,
        DeploymentComparisonReportError::DigestMismatch {
            field: "report_digest"
        }
    );
}

#[test]
fn deployment_comparison_report_requires_target_deployment_identity() {
    let left = sample_check(sample_plan(), sample_matching_inventory());
    let right = sample_check(sample_plan(), sample_matching_inventory());
    let mut report = deployment_comparison_report_from_checks(
        "comparison-1",
        "2026-05-26T00:00:00Z",
        "left",
        "right",
        &left,
        &right,
    );

    report.left.deployment_identity.deployment_name.clear();

    let err = validate_deployment_comparison_report(&report)
        .expect_err("missing comparison target deployment name should fail");
    assert_eq!(
        err,
        DeploymentComparisonReportError::MissingRequiredField {
            field: "left.deployment_identity.deployment_name"
        }
    );
}

#[test]
fn deployment_comparison_report_blocks_stale_input_diff() {
    let mut left = sample_check(sample_plan(), sample_matching_inventory());
    left.diff.warnings.push(SafetyFindingV1 {
        code: "stale_warning".to_string(),
        message: "stale warning".to_string(),
        severity: SafetySeverityV1::Warning,
        subject: Some("root".to_string()),
    });
    let right = sample_check(sample_plan(), sample_matching_inventory());

    let report = deployment_comparison_report_from_checks(
        "comparison-1",
        "2026-05-26T00:00:00Z",
        "left",
        "right",
        &left,
        &right,
    );

    assert_eq!(report.status, SafetyStatusV1::Blocked);
    assert!(
        report
            .hard_failures
            .iter()
            .any(|failure| failure.code == "deployment_comparison_input_diff_stale")
    );
    validate_deployment_comparison_report(&report).expect("comparison should validate");
}

#[test]
fn deployment_comparison_report_blocks_stale_input_report() {
    let mut left_plan = sample_plan();
    left_plan
        .unresolved_assumptions
        .push(DeploymentAssumptionV1 {
            key: "local_state.root_canister_id".to_string(),
            description: "root identity is unknown until install".to_string(),
        });
    let mut left = sample_check(left_plan, sample_matching_inventory());
    left.report.summary = "stale report summary".to_string();
    let right = sample_check(sample_plan(), sample_matching_inventory());

    let report = deployment_comparison_report_from_checks(
        "comparison-1",
        "2026-05-26T00:00:00Z",
        "left",
        "right",
        &left,
        &right,
    );

    assert_eq!(report.status, SafetyStatusV1::Blocked);
    assert!(
        report
            .hard_failures
            .iter()
            .any(|failure| failure.code == "deployment_comparison_input_report_stale")
    );
    validate_deployment_comparison_report(&report).expect("comparison should validate");
}

#[test]
fn deployment_comparison_report_preserves_blocked_input_status() {
    let mut left_inventory = sample_matching_inventory();
    left_inventory.observed_canisters[0].control_class = CanisterControlClassV1::UserControlled;
    left_inventory.observed_canisters[0].controllers = vec!["user-principal".to_string()];
    let left = sample_check(sample_plan(), left_inventory);
    assert_eq!(left.report.status, SafetyStatusV1::Blocked);
    let right = sample_check(sample_plan(), sample_matching_inventory());

    let report = deployment_comparison_report_from_checks(
        "comparison-1",
        "2026-05-26T00:00:00Z",
        "left",
        "right",
        &left,
        &right,
    );

    assert_eq!(report.status, SafetyStatusV1::Blocked);
    assert!(report.identity_diff.is_empty());
    assert!(
        report
            .hard_failures
            .iter()
            .any(|failure| failure.code == "deployment_comparison_input_blocked")
    );
    assert!(
        report
            .next_actions
            .iter()
            .any(|action| action.contains("resolve hard comparison failures"))
    );
    validate_deployment_comparison_report(&report).expect("comparison should validate");
}

#[test]
fn deployment_comparison_report_preserves_warning_input_status() {
    let mut left_plan = sample_plan();
    left_plan
        .unresolved_assumptions
        .push(DeploymentAssumptionV1 {
            key: "local_state.root_canister_id".to_string(),
            description: "root identity is unknown until install".to_string(),
        });
    let left = sample_check(left_plan, sample_matching_inventory());
    assert_eq!(left.report.status, SafetyStatusV1::Warning);
    let right = sample_check(sample_plan(), sample_matching_inventory());

    let report = deployment_comparison_report_from_checks(
        "comparison-1",
        "2026-05-26T00:00:00Z",
        "left",
        "right",
        &left,
        &right,
    );

    assert_eq!(report.status, SafetyStatusV1::Warning);
    assert!(
        report
            .warnings
            .iter()
            .any(|warning| warning.code == "deployment_comparison_input_warning")
    );
    validate_deployment_comparison_report(&report).expect("comparison should validate");
}

#[test]
fn deployment_comparison_report_text_is_passive() {
    let mut left = sample_check(sample_plan(), sample_matching_inventory());
    left.report.summary = "stale report summary".to_string();
    let right = sample_check(sample_plan(), sample_matching_inventory());
    let report = deployment_comparison_report_from_checks(
        "comparison-1",
        "2026-05-26T00:00:00Z",
        "left",
        "right",
        &left,
        &right,
    );

    let text = deployment_comparison_report_text(&report);

    assert!(text.contains("Deployment comparison report"));
    assert!(text.contains("mode: passive"));
    assert!(text.contains("execution: none"));
    assert!(text.contains("external_lifecycle: 0"));
    assert!(text.contains("hard_failures:"));
    assert!(text.contains("deployment_comparison_input_report_stale"));
    assert!(text.contains("next_actions:"));
}