canic-host 0.110.51

Host-side App build and desired-state Fleet reconciliation library for Canic workspaces
Documentation
//! Host receipt admission rejects substituted identities, incomplete phases and false accounting.

use super::*;
use crate::fleet_ensure::policy::capacity_import::tests::{plan, principal};
use canic_core::dto::pool_import::{PoolImportRootReceipt, PoolImportSourceReceipt};

pub(in crate::fleet_ensure) fn settled(plan: &CapacityImportPlanRecord) -> PoolImportStatus {
    PoolImportStatus {
        reserved_at_ns: 1,
        reservation: root_reservation(plan).unwrap(),
        progress: plan
            .sources
            .iter()
            .map(|source| {
                PoolImportSourceProgress::Ready(PoolImportSourceReceipt {
                    root_sender_canister_version: 17,
                    canister_id: source.binding.canister_id,
                    canister_version: source.binding.canister_version + 3,
                    before_uninstall_canister_version: source.binding.canister_version + 2,
                    retained_cycles: source.observed_cycles - 20,
                    retained_reserved_cycles: source.observed_reserved_cycles - 10,
                    observed_debit_cycles: 30,
                })
            })
            .collect(),
        phase: PoolImportPhase::Ready,
        paid_calls: 8,
        reserved_debit_cycles: 50,
        last_root_cycles: plan.root_budget.observed_cycles - 20,
        root_receipt: Some(PoolImportRootReceipt {
            retained_cycles: plan.root_budget.observed_cycles - 20,
            retained_reserved_cycles: plan.root_budget.observed_reserved_cycles - 10,
            observed_debit_cycles: 30,
        }),
    }
}

#[test]
fn running_root_receipt_binds_observed_stopped_version_and_exact_uninstall() {
    let mut selected = plan();
    selected.sources[0].binding.controllers = vec![selected.authority.root];
    selected.sources[0].binding.stopped = false;
    let selected = crate::fleet_ensure::ops::capacity_import::prepare_review(
        selected.authority,
        selected.sources,
        selected.root_budget,
    )
    .unwrap();
    let mut status = settled(&selected);
    let PoolImportSourceProgress::Ready(receipt) = &mut status.progress[0] else {
        unreachable!()
    };
    receipt.before_uninstall_canister_version += 20;
    receipt.canister_version = receipt.before_uninstall_canister_version + 1;
    validate_root_status(&selected, &status).unwrap();
    let PoolImportSourceProgress::Ready(receipt) = &mut status.progress[0] else {
        unreachable!()
    };
    receipt.canister_version += 1;
    assert!(matches!(
        validate_root_status(&selected, &status),
        Err(CapacityImportReviewError::RootEvidenceMismatch)
    ));
}

#[test]
fn capacity_import_root_evidence_requires_complete_correlated_terminal_receipts() {
    let plan = plan();
    let valid = settled(&plan);
    validate_root_status(&plan, &valid).unwrap();
    let mutations: [fn(&mut PoolImportStatus); 6] = [
        |s| s.progress[0] = PoolImportSourceProgress::UninstallIssued,
        |s| s.phase = PoolImportPhase::Reserved,
        |s| s.paid_calls = u32::MAX,
        |s| s.reserved_debit_cycles = u128::MAX,
        |s| s.last_root_cycles -= 1,
        |s| {
            s.phase = PoolImportPhase::Released {
                publication_sha256: [0; 32],
            }
        },
    ];
    for change in mutations {
        let mut wrong = valid.clone();
        change(&mut wrong);
        assert!(matches!(
            validate_root_status(&plan, &wrong),
            Err(CapacityImportReviewError::RootEvidenceMismatch)
        ));
    }
    let mut released = valid;
    released.phase = PoolImportPhase::Released {
        publication_sha256: [1; 32],
    };
    validate_root_status(&plan, &released).unwrap();
    released.root_receipt = None;
    assert!(matches!(
        validate_root_status(&plan, &released),
        Err(CapacityImportReviewError::RootEvidenceMismatch)
    ));
}

#[test]
fn capacity_import_root_evidence_accounts_native_reserved_and_exact_original_debit() {
    let plan = plan();
    let valid = settled(&plan);
    let mutations: [fn(&mut PoolImportSourceReceipt); 7] = [
        |r| r.canister_id = principal(99),
        |r| r.canister_version += 1,
        |r| r.retained_reserved_cycles += 1,
        |r| r.retained_cycles += 1,
        |r| r.observed_debit_cycles += 1,
        |r| {
            r.retained_cycles = 700;
            r.retained_reserved_cycles = 370;
        },
        |r| r.retained_cycles = u128::MAX,
    ];
    for change in mutations {
        let mut wrong = valid.clone();
        let PoolImportSourceProgress::Ready(receipt) = &mut wrong.progress[0] else {
            unreachable!()
        };
        change(receipt);
        assert!(matches!(
            validate_root_status(&plan, &wrong),
            Err(CapacityImportReviewError::RootEvidenceMismatch)
        ));
    }
    let mut wrong_root = valid;
    wrong_root
        .root_receipt
        .as_mut()
        .unwrap()
        .retained_reserved_cycles += 1;
    assert!(matches!(
        validate_root_status(&plan, &wrong_root),
        Err(CapacityImportReviewError::RootEvidenceMismatch)
    ));
}

#[test]
fn capacity_import_root_evidence_accepts_partial_progress_without_claiming_completion() {
    let plan = plan();
    let mut partial = settled(&plan);
    partial.phase = PoolImportPhase::Reserved;
    partial.progress[0] = PoolImportSourceProgress::ControllersIssued;
    partial.root_receipt = None;
    validate_root_status(&plan, &partial).unwrap();
    partial.reservation.root = principal(100);
    assert!(matches!(
        validate_root_status(&plan, &partial),
        Err(CapacityImportReviewError::ReservationMismatch)
    ));
}

#[test]
fn capacity_import_root_evidence_accepts_net_surplus_without_rebasing_authority() {
    let plan = plan();
    let baseline = serde_json::to_vec(&plan).unwrap();
    let mut retained = settled(&plan);
    retained.last_root_cycles = plan.root_budget.observed_cycles + 100;
    let root = retained.root_receipt.as_mut().unwrap();
    root.retained_cycles = retained.last_root_cycles;
    root.retained_reserved_cycles = plan.root_budget.observed_reserved_cycles;
    root.observed_debit_cycles = 0;
    for (source, progress) in plan.sources.iter().zip(&mut retained.progress) {
        let PoolImportSourceProgress::Ready(receipt) = progress else {
            unreachable!()
        };
        receipt.retained_cycles = source.observed_cycles + 100;
        receipt.retained_reserved_cycles = source.observed_reserved_cycles;
        receipt.observed_debit_cycles = 0;
    }
    validate_root_status(&plan, &retained).unwrap();
    let mut partial = retained.clone();
    partial.root_receipt = None;
    partial.progress[0] = PoolImportSourceProgress::UninstallIssued;
    partial.phase = PoolImportPhase::Reserved;
    validate_root_status(&plan, &partial).unwrap();
    retained
        .root_receipt
        .as_mut()
        .unwrap()
        .observed_debit_cycles = 1;
    assert!(matches!(
        validate_root_status(&plan, &retained),
        Err(CapacityImportReviewError::RootEvidenceMismatch)
    ));
    assert_eq!(serde_json::to_vec(&plan).unwrap(), baseline);
}