canic-host 0.111.0

Host-side App build and desired-state Fleet reconciliation library for Canic workspaces
Documentation
//! Exact owner/store-qualified pagination and bounded decoding of canonical accounting.

use super::*;
use canic_contracts::dto::{
    release_intents::{IntentReleaseReceipt, IntentReleaseReceiptState},
    release_receipts::{ReplayReleaseIntent, ReplayReleaseIntentState},
};

pub(in crate::fleet_ensure::ops::release) fn fixture(owner: Principal) -> IntentReleaseResponse {
    IntentReleaseResponse {
        owner,
        entry: Some(IntentReleaseEntry::Local {
            intent_id: 1,
            record: ReplayReleaseIntent {
                resource_key: "canic:cycles:orphan".into(),
                quantity: u64::MAX,
                state: ReplayReleaseIntentState::Pending,
                created_at_secs: 1,
                ttl_secs: Some(1),
            },
        }),
        next_after: None,
    }
}

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

#[test]
fn pages_keep_original_accounting_across_the_store_boundary() {
    let owner = Principal::from_slice(&[1]);
    let mut local = fixture(owner);
    local.next_after = Some(IntentReleaseKey::Local(1));
    let receipt = IntentReleaseResponse {
        owner,
        entry: Some(IntentReleaseEntry::ReceiptBacked(IntentReleaseReceipt {
            operation_id: [0; 32],
            payload_hash_schema_version: 1,
            payload_hash: [2; 32],
            resource_key: "application:pending".into(),
            quantity: 9,
            state: IntentReleaseReceiptState::Pending,
            revision: 3,
            created_at_ns: 1,
            updated_at_ns: 2,
            application_replay_deadline_ns: Some(3),
        })),
        next_after: None,
    };
    let mut pages = Pages::new(owner);
    let mut remaining = MAXIMUM_CENSUS_BYTES;
    assert!(
        !pages
            .push(decode_response(owner, &wire(local.clone()), &mut remaining).unwrap())
            .unwrap()
    );
    assert!(
        pages
            .push(decode_response(owner, &wire(receipt.clone()), &mut remaining).unwrap())
            .unwrap()
    );
    assert_eq!(pages.pages, vec![local, receipt]);
}

#[test]
fn rejects_wrong_owner_cursor_regression_and_false_completion() {
    let owner = Principal::from_slice(&[1]);
    let mut wrong_owner = fixture(Principal::anonymous());
    assert_eq!(
        Pages::new(owner).push(wrong_owner.clone()),
        Err(ReleaseIntentsStage::Binding)
    );
    wrong_owner.owner = owner;
    wrong_owner.next_after = Some(IntentReleaseKey::ReceiptBacked([1; 32]));
    assert_eq!(
        Pages::new(owner).push(wrong_owner),
        Err(ReleaseIntentsStage::Pagination)
    );
    let mut first = fixture(owner);
    first.next_after = Some(IntentReleaseKey::Local(1));
    let mut pages = Pages::new(owner);
    assert!(!pages.push(first).unwrap());
    assert_eq!(
        pages.push(fixture(owner)),
        Err(ReleaseIntentsStage::Pagination)
    );
    assert_eq!(
        pages.push(IntentReleaseResponse {
            owner,
            entry: None,
            next_after: None
        }),
        Err(ReleaseIntentsStage::Pagination)
    );
    pages.cursor = Some(IntentReleaseKey::ReceiptBacked([0; 32]));
    assert_eq!(
        pages.push(fixture(owner)),
        Err(ReleaseIntentsStage::Pagination)
    );
}

#[test]
fn decoding_and_collection_bounds_refuse_without_returning_partial_evidence() {
    let owner = Principal::anonymous();
    let bytes = wire(fixture(owner));
    for (bytes, mut budget, stage) in [
        (
            vec![0; RESPONSE_BYTES + 1],
            MAXIMUM_CENSUS_BYTES,
            ReleaseIntentsStage::Decode,
        ),
        (vec![0], MAXIMUM_CENSUS_BYTES, ReleaseIntentsStage::Decode),
        (bytes.clone(), bytes.len() - 1, ReleaseIntentsStage::Budget),
    ] {
        assert!(
            matches!(decode_response(owner, &bytes, &mut budget), Err(ReleaseIntentsError::Observation { stage: actual, .. }) if actual == stage)
        );
    }
    let mut pages = Pages::new(owner);
    pages.pages = vec![fixture(owner); MAXIMUM_INTENTS - 1];
    let mut page = fixture(owner);
    page.next_after = Some(IntentReleaseKey::Local(1));
    assert_eq!(pages.push(page), Err(ReleaseIntentsStage::Budget));
    let empty = IntentReleaseResponse {
        owner,
        entry: None,
        next_after: None,
    };
    assert!(Pages::new(owner).push(empty).unwrap());
}