canic-host 0.110.52

Host-side App build and desired-state Fleet reconciliation library for Canic workspaces
Documentation
//! Retained uncertainty, exact pagination and bounded wire decoding.

use super::*;
use crate::fleet_ensure::{
    view::release::receipts::ReleaseReplayDisposition, workflow::release::assess_receipts,
};
use canic_core::dto::release_receipts::{
    ReplayReleaseAuthentication, ReplayReleaseEffect, ReplayReleaseEntry, ReplayReleaseIntent,
    ReplayReleaseIntentState, ReplayReleasePhase, ReplayReleaseRecoveryReason,
    ReplayReleaseSettlement,
};

pub(in crate::fleet_ensure::ops::release) fn fixture(owner: Principal) -> ReplayReleaseResponse {
    ReplayReleaseResponse {
        owner,
        entry: Some(ReplayReleaseEntry {
            slot: [1; 32],
            command_kind: "root.request_cycles.v1".into(),
            operation_id: [2; 32],
            actor: Principal::from_slice(&[3]),
            authentication: ReplayReleaseAuthentication::DirectCaller,
            payload_hash_schema_version: 1,
            payload_hash: [4; 32],
            phase: ReplayReleasePhase::RecoveryRequired(
                ReplayReleaseRecoveryReason::CostSettlementFailed,
            ),
            created_at_ns: 1,
            updated_at_ns: 2,
            expires_at_ns: Some(3),
            cost_guard_settlement: Some(ReplayReleaseSettlement {
                quota_intent_id: 5,
                reservation_intent_id: 6,
                quota: Some(ReplayReleaseIntent {
                    resource_key: "canic:quota:test".into(),
                    quantity: 1,
                    state: ReplayReleaseIntentState::Committed,
                    created_at_secs: 1,
                    ttl_secs: Some(1),
                }),
                reservation: Some(ReplayReleaseIntent {
                    resource_key: "canic:cycles:test".into(),
                    quantity: u64::MAX,
                    state: ReplayReleaseIntentState::Pending,
                    created_at_secs: 1,
                    ttl_secs: None,
                }),
            }),
            effect: Some(ReplayReleaseEffect::ManagementCall {
                canister: Principal::from_slice(&[7]),
                method: "deposit_cycles".into(),
            }),
        }),
        next_after: None,
    }
}

pub(in crate::fleet_ensure::ops::release) fn wire(page: ReplayReleaseResponse) -> Vec<u8> {
    candid::encode_one(Ok::<_, CanicError>(Response::ReplayRelease(page))).unwrap()
}

#[test]
fn assessment_preserves_wire_evidence_and_separates_completion_from_accounting_drift() {
    let owner = Principal::from_slice(&[1]);
    let other = Principal::from_slice(&[2]);
    let empty_owner = Principal::from_slice(&[3]);
    let mut uncertain = fixture(owner);
    let entry = uncertain.entry.as_mut().unwrap();
    entry.phase = ReplayReleasePhase::ExternalEffectInFlight;
    entry
        .cost_guard_settlement
        .as_mut()
        .unwrap()
        .reservation
        .as_mut()
        .unwrap()
        .state = ReplayReleaseIntentState::Aborted;
    uncertain.next_after = Some(entry.slot);
    let mut completed = fixture(owner);
    let entry = completed.entry.as_mut().unwrap();
    entry.slot = [9; 32];
    entry.phase = ReplayReleasePhase::Committed;
    entry.cost_guard_settlement.as_mut().unwrap().quota = None;
    let empty = ReplayReleaseResponse {
        owner: empty_owner,
        entry: None,
        next_after: None,
    };
    let originals = FleetReleaseReceiptsView {
        owners: [
            (owner, vec![uncertain, completed]),
            (other, vec![fixture(other)]),
            (empty_owner, vec![empty]),
        ]
        .into(),
    };
    let mut remaining = MAXIMUM_CENSUS_BYTES;
    let evidence = FleetReleaseReceiptsView {
        owners: originals
            .owners
            .iter()
            .map(|(owner, pages)| {
                (
                    *owner,
                    pages
                        .iter()
                        .map(|page| {
                            decode_response(*owner, &wire(page.clone()), &mut remaining).unwrap()
                        })
                        .collect(),
                )
            })
            .collect(),
    };
    let result = assess_receipts(evidence);
    assert_eq!(result.evidence, originals);
    let assessments = &result.owners[&owner];
    assert_eq!(assessments.len(), 2);
    assert_eq!(
        assessments[0].disposition,
        ReleaseReplayDisposition::EffectReconciliation
    );
    assert!(assessments[0].pending_intents.is_empty());
    assert_eq!(
        assessments[1].disposition,
        ReleaseReplayDisposition::RecordedCompletion
    );
    assert_eq!(assessments[1].missing_intents, vec![5]);
    assert_eq!(assessments[1].pending_intents, vec![6]);
    assert_ne!(assessments[0].slot, assessments[1].slot);
    assert_eq!(assessments[0].operation_id, assessments[1].operation_id);
    assert_eq!(
        result.owners[&other][0].disposition,
        ReleaseReplayDisposition::AccountingRecovery
    );
    assert!(result.owners[&empty_owner].is_empty());
}

#[test]
fn preserves_expired_uncertainty_original_actor_and_accounting_including_terminal_history() {
    let owner = Principal::from_slice(&[1]);
    let mut first = fixture(owner);
    first.next_after = Some(first.entry.as_ref().unwrap().slot);
    let mut terminal = fixture(owner);
    terminal.entry.as_mut().unwrap().slot = [9; 32];
    terminal.entry.as_mut().unwrap().phase = ReplayReleasePhase::Committed;
    let mut pages = Pages::new(owner);
    let mut remaining = MAXIMUM_CENSUS_BYTES;
    for page in [&first, &terminal] {
        let bytes = wire(page.clone());
        let decoded = decode_response(owner, &bytes, &mut remaining).unwrap();
        assert_eq!(&decoded, page);
        assert_eq!(pages.push(decoded).unwrap(), page.next_after.is_none());
    }
    assert_eq!(pages.pages, vec![first, terminal]);
}

#[test]
fn refuses_foreign_owners_and_nonadvancing_missing_or_mismatched_rows_without_partial_admission() {
    let owner = Principal::from_slice(&[1]);
    let mut first = fixture(owner);
    first.next_after = Some(first.entry.as_ref().unwrap().slot);
    let mut cases = Vec::new();
    let mut invalid = fixture(Principal::anonymous());
    cases.push((invalid, ReleaseReceiptsStage::Binding));
    cases.push((fixture(owner), ReleaseReceiptsStage::Pagination));
    invalid = fixture(owner);
    invalid.entry = None;
    cases.push((invalid, ReleaseReceiptsStage::Pagination));
    invalid = fixture(owner);
    invalid.entry.as_mut().unwrap().slot = [9; 32];
    invalid.next_after = Some([8; 32]);
    cases.push((invalid, ReleaseReceiptsStage::Pagination));
    for (invalid, expected) in cases {
        let mut pages = Pages::new(owner);
        assert!(!pages.push(first.clone()).unwrap());
        assert_eq!(pages.push(invalid), Err(expected));
        assert_eq!(pages.pages, vec![first.clone()]);
    }
}

#[test]
fn exact_record_allowance_requires_a_terminal_page() {
    let owner = Principal::from_slice(&[1]);
    let mut pages = Pages::new(owner);
    for index in 0..MAXIMUM_RECEIPTS {
        let mut slot = [0; 32];
        slot[..8].copy_from_slice(&u64::try_from(index).unwrap().to_be_bytes());
        let mut page = fixture(owner);
        page.entry.as_mut().unwrap().slot = slot;
        page.next_after = Some(slot);
        if index == MAXIMUM_RECEIPTS - 1 {
            assert_eq!(pages.push(page.clone()), Err(ReleaseReceiptsStage::Budget));
            page.next_after = None;
            assert!(pages.push(page).unwrap());
        } else {
            assert!(!pages.push(page).unwrap());
        }
    }
    assert_eq!(pages.pages.len(), MAXIMUM_RECEIPTS);
}

#[test]
fn rejects_raw_aggregate_and_skipping_budget_exhaustion() {
    #[derive(CandidType)]
    enum Extended {
        ReplayRelease { owner: Principal, extra: Vec<()> },
    }
    let owner = Principal::from_slice(&[1]);
    let bytes = wire(fixture(owner));
    assert!(matches!(
        decode_response(owner, &bytes, &mut 0),
        Err(ReleaseReceiptsError::Observation {
            stage: ReleaseReceiptsStage::Budget,
            ..
        })
    ));
    let skipped = candid::encode_one(Ok::<_, CanicError>(Extended::ReplayRelease {
        owner,
        extra: vec![(); RESPONSE_BYTES * 8 + 1],
    }))
    .unwrap();
    assert!(skipped.len() < RESPONSE_BYTES);
    assert!(
        candid::decode_one::<Result<Response, CanicError>>(&skipped)
            .unwrap()
            .is_ok()
    );
    for bytes in [b"DIDL".to_vec(), vec![0; RESPONSE_BYTES + 1], skipped] {
        let mut remaining = MAXIMUM_CENSUS_BYTES;
        assert!(matches!(
            decode_response(owner, &bytes, &mut remaining),
            Err(ReleaseReceiptsError::Observation {
                stage: ReleaseReceiptsStage::Decode,
                ..
            })
        ));
    }
}

#[test]
fn distinguishes_empty_inventory_from_typed_refusal() {
    let owner = Principal::from_slice(&[1]);
    let empty = ReplayReleaseResponse {
        owner,
        entry: None,
        next_after: None,
    };
    let mut remaining = MAXIMUM_CENSUS_BYTES;
    assert!(
        Pages::new(owner)
            .push(decode_response(owner, &wire(empty), &mut remaining).unwrap())
            .unwrap()
    );
    let rejected =
        CanicError::from_registered(canic_core::diagnostics::codes::AUTHORITY_UNAVAILABLE);
    let bytes = candid::encode_one(Err::<Response, _>(rejected)).unwrap();
    assert!(
        matches!(decode_response(owner, &bytes, &mut remaining), Err(ReleaseReceiptsError::Rejected { rejection, .. }) if rejection.code() == rejected.code())
    );
}