canic-host 0.70.6

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

#[test]
fn lifecycle_authority_report_projects_deployment_controlled_roles() {
    let check = sample_check(sample_plan(), sample_matching_inventory());

    let report = lifecycle_authority_report_from_check("lifecycle-authority-1", &check);

    assert_eq!(report.schema_version, DEPLOYMENT_TRUTH_SCHEMA_VERSION);
    assert_eq!(report.report_id, "lifecycle-authority-1");
    assert_eq!(report.report_digest.len(), 64);
    assert_eq!(report.check_id, "check-1");
    assert_eq!(report.authorities.len(), 1);
    assert_eq!(report.external_action_required_count, 0);
    assert_eq!(report.blocked_count, 0);
    let authority = &report.authorities[0];
    assert_eq!(authority.role.as_deref(), Some("root"));
    assert_eq!(
        authority.control_class,
        CanisterControlClassV1::DeploymentControlled
    );
    assert_eq!(
        authority.lifecycle_mode,
        LifecycleModeV1::DirectDeploymentAuthority
    );
    assert_eq!(authority.required_controllers, vec!["aaaaa-aa"]);
    assert_eq!(authority.expected_deployment_controllers, vec!["aaaaa-aa"]);
    assert!(authority.external_controllers.is_empty());
    assert!(authority.consent_requirements.is_empty());
    assert!(
        authority
            .allowed_upgrade_modes
            .contains(&LifecycleUpgradeModeV1::DirectByDeploymentAuthority)
    );
    assert!(
        authority
            .verification_requirements
            .contains(&LifecycleVerificationRequirementV1::ProtectedCallReadiness)
    );
    assert_json_round_trip(&report);
    validate_lifecycle_authority_report(&report).expect("lifecycle report should validate");
}

#[test]
fn lifecycle_authority_report_projects_user_controlled_roles_as_external() {
    let mut plan = sample_plan();
    plan.expected_canisters[0].control_class = CanisterControlClassV1::UserControlled;
    let mut inventory = sample_matching_inventory();
    inventory.observed_canisters[0].control_class = CanisterControlClassV1::UserControlled;
    inventory.observed_canisters[0].controllers = vec!["user-principal".to_string()];
    let check = sample_check(plan, inventory);

    let report = lifecycle_authority_report_from_check("lifecycle-authority-1", &check);

    assert_eq!(report.external_action_required_count, 1);
    assert_eq!(report.blocked_count, 0);
    let authority = &report.authorities[0];
    assert_eq!(
        authority.control_class,
        CanisterControlClassV1::UserControlled
    );
    assert_eq!(
        authority.lifecycle_mode,
        LifecycleModeV1::ExternalCompletionOnly
    );
    assert_eq!(authority.external_controllers, vec!["user-principal"]);
    assert_eq!(
        authority.consent_requirements[0].required_principals,
        vec!["user-principal"]
    );
    assert!(authority.external_action_required);
    assert!(!authority.blocked);
    assert!(
        !authority
            .allowed_upgrade_modes
            .contains(&LifecycleUpgradeModeV1::DirectByDeploymentAuthority)
    );
    assert!(
        authority
            .allowed_upgrade_modes
            .contains(&LifecycleUpgradeModeV1::ExternalProposal)
    );
    assert!(
        authority
            .allowed_upgrade_modes
            .contains(&LifecycleUpgradeModeV1::VerifyExternalCompletion)
    );
    validate_lifecycle_authority_report(&report).expect("lifecycle report should validate");
}

#[test]
fn lifecycle_authority_report_blocks_unknown_unsafe_roles() {
    let mut plan = sample_plan();
    plan.expected_canisters[0].control_class = CanisterControlClassV1::UnknownUnsafe;
    let mut inventory = sample_matching_inventory();
    inventory.observed_canisters[0].control_class = CanisterControlClassV1::UnknownUnsafe;
    let check = sample_check(plan, inventory);

    let report = lifecycle_authority_report_from_check("lifecycle-authority-1", &check);

    assert_eq!(report.external_action_required_count, 0);
    assert_eq!(report.blocked_count, 1);
    let authority = &report.authorities[0];
    assert_eq!(
        authority.control_class,
        CanisterControlClassV1::UnknownUnsafe
    );
    assert_eq!(
        authority.allowed_upgrade_modes,
        vec![LifecycleUpgradeModeV1::Blocked]
    );
    assert!(authority.blocked);
    assert_eq!(
        authority.lifecycle_mode,
        LifecycleModeV1::UnknownUnsafeBlocked
    );
    assert!(!authority.blockers.is_empty());
}

#[test]
fn lifecycle_authority_report_validation_rejects_count_and_digest_drift() {
    let check = sample_check(sample_plan(), sample_matching_inventory());
    let mut report = lifecycle_authority_report_from_check("lifecycle-authority-1", &check);
    report.external_action_required_count = 4;

    let err = validate_lifecycle_authority_report(&report).expect_err("count drift should fail");
    std::assert_matches!(err, LifecycleAuthorityReportError::CountMismatch);

    let mut report = lifecycle_authority_report_from_check("lifecycle-authority-1", &check);
    report.authorities[0]
        .warnings
        .push("stale warning".to_string());
    let err = validate_lifecycle_authority_report(&report).expect_err("digest drift should fail");
    std::assert_matches!(
        err,
        LifecycleAuthorityReportError::DigestMismatch {
            field: "report_digest"
        }
    );
}

#[test]
fn lifecycle_authority_report_text_reports_passive_boundary() {
    let mut plan = sample_plan();
    plan.expected_canisters[0].control_class = CanisterControlClassV1::UserControlled;
    let mut inventory = sample_matching_inventory();
    inventory.observed_canisters[0].control_class = CanisterControlClassV1::UserControlled;
    inventory.observed_canisters[0].controllers = vec!["user-principal".to_string()];
    let check = sample_check(plan, inventory);
    let report = lifecycle_authority_report_from_check("lifecycle-authority-1", &check);

    let text = lifecycle_authority_report_text(&report);

    assert!(text.contains("Lifecycle authority report"));
    assert!(text.contains("mode: passive"));
    assert!(text.contains("execution: none"));
    assert!(text.contains("report_digest:"));
    assert!(text.contains("external_action_required: 1"));
    assert!(text.contains("lifecycle_mode=external_completion_only"));
}

#[test]
fn external_lifecycle_plan_partitions_roles_before_proposals() {
    let mut plan = sample_plan();
    plan.expected_canisters[0].control_class = CanisterControlClassV1::UserControlled;
    let mut inventory = sample_matching_inventory();
    inventory.observed_canisters[0].control_class = CanisterControlClassV1::UserControlled;
    inventory.observed_canisters[0].controllers = vec!["user-principal".to_string()];
    let check = sample_check(plan, inventory);

    let lifecycle_plan = external_lifecycle_plan_from_check(
        "external-lifecycle-plan-1",
        "lifecycle-authority-1",
        &check,
    );

    assert_eq!(
        lifecycle_plan.schema_version,
        DEPLOYMENT_TRUTH_SCHEMA_VERSION
    );
    assert_eq!(
        lifecycle_plan.lifecycle_plan_id,
        "external-lifecycle-plan-1"
    );
    assert_eq!(
        lifecycle_plan.lifecycle_authority_report_id,
        "lifecycle-authority-1"
    );
    assert_eq!(
        lifecycle_plan.status,
        ExternalLifecyclePlanStatusV1::PendingExternalAction
    );
    assert_eq!(lifecycle_plan.lifecycle_plan_digest.len(), 64);
    assert!(lifecycle_plan.directly_executable_role_upgrades.is_empty());
    assert_eq!(lifecycle_plan.proposed_external_role_upgrades.len(), 1);
    assert!(lifecycle_plan.blocked_role_upgrades.is_empty());
    assert!(!lifecycle_plan.residual_exposure.is_empty());
    validate_external_lifecycle_plan(&lifecycle_plan).expect("lifecycle plan should validate");
    validate_external_lifecycle_plan_for_check(&lifecycle_plan, &check)
        .expect("lifecycle plan should match source check");
    assert_json_round_trip(&lifecycle_plan);
}

#[test]
fn external_lifecycle_plan_validation_rejects_digest_and_status_drift() {
    let check = sample_check(sample_plan(), sample_matching_inventory());
    let mut plan = external_lifecycle_plan_from_check(
        "external-lifecycle-plan-1",
        "lifecycle-authority-1",
        &check,
    );
    plan.lifecycle_plan_digest = sample_sha256("9");

    let err = validate_external_lifecycle_plan(&plan).expect_err("stale digest should fail");
    std::assert_matches!(
        err,
        ExternalLifecyclePlanError::DigestMismatch {
            field: "lifecycle_plan_digest"
        }
    );

    plan = external_lifecycle_plan_from_check(
        "external-lifecycle-plan-1",
        "lifecycle-authority-1",
        &check,
    );
    plan.status = ExternalLifecyclePlanStatusV1::Blocked;
    plan.lifecycle_plan_digest = sample_sha256("9");
    let err = validate_external_lifecycle_plan(&plan).expect_err("status drift should fail");
    std::assert_matches!(err, ExternalLifecyclePlanError::DigestMismatch { .. });
}

#[test]
fn external_lifecycle_plan_validation_rejects_source_check_drift() {
    let mut plan = sample_plan();
    plan.expected_canisters[0].control_class = CanisterControlClassV1::UserControlled;
    let mut inventory = sample_matching_inventory();
    inventory.observed_canisters[0].control_class = CanisterControlClassV1::UserControlled;
    inventory.observed_canisters[0].controllers = vec!["user-principal".to_string()];
    let check = sample_check(plan, inventory);
    let lifecycle_plan = external_lifecycle_plan_from_check(
        "external-lifecycle-plan-1",
        "lifecycle-authority-1",
        &check,
    );
    let mut drifted_check = check;
    drifted_check.inventory.observed_canisters[0]
        .controllers
        .push("other-controller".to_string());

    let err = validate_external_lifecycle_plan_for_check(&lifecycle_plan, &drifted_check)
        .expect_err("source check drift should fail");
    std::assert_matches!(
        err,
        ExternalLifecyclePlanError::SourceMismatch {
            field: "deployment_check"
        }
    );
}

#[test]
fn external_lifecycle_plan_text_reports_passive_boundary() {
    let mut plan = sample_plan();
    plan.expected_canisters[0].control_class = CanisterControlClassV1::UserControlled;
    let mut inventory = sample_matching_inventory();
    inventory.observed_canisters[0].control_class = CanisterControlClassV1::UserControlled;
    inventory.observed_canisters[0].controllers = vec!["user-principal".to_string()];
    let check = sample_check(plan, inventory);
    let lifecycle_plan = external_lifecycle_plan_from_check(
        "external-lifecycle-plan-1",
        "lifecycle-authority-1",
        &check,
    );

    let text = external_lifecycle_plan_text(&lifecycle_plan);

    assert!(text.contains("External lifecycle plan"));
    assert!(text.contains("mode: passive"));
    assert!(text.contains("execution: none"));
    assert!(text.contains("status: pending_external_action"));
    assert!(text.contains("proposed_external: 1"));
    assert!(text.contains("proposed_external_role_upgrades"));
    assert!(text.contains("required_external_action=external_controller_execution"));
}

#[test]
fn lifecycle_authority_report_json_shape_is_stable() {
    let check = sample_check(sample_plan(), sample_matching_inventory());
    let report = lifecycle_authority_report_from_check("lifecycle-authority-1", &check);
    let encoded = serde_json::to_value(&report).expect("report should encode");

    assert_object_keys(
        &encoded,
        &[
            "schema_version",
            "report_id",
            "report_digest",
            "check_id",
            "plan_id",
            "inventory_id",
            "authorities",
            "external_action_required_count",
            "blocked_count",
        ],
    );
    assert_object_keys(
        &encoded["authorities"][0],
        &[
            "subject",
            "canister_id",
            "role",
            "control_class",
            "lifecycle_mode",
            "observed_controllers",
            "expected_deployment_controllers",
            "external_controllers",
            "required_controllers",
            "consent_requirements",
            "allowed_upgrade_modes",
            "verification_requirements",
            "external_action_required",
            "blocked",
            "blockers",
            "warnings",
            "reason",
        ],
    );
}