canic-host 0.110.51

Host-side App build and desired-state Fleet reconciliation library for Canic workspaces
Documentation
//! Physical sampling uses real certified management observations; no role or account assertions.

use super::*;
use crate::{
    fleet_ensure::{
        model::capacity_import::survey::CapacityImportSampleRecord,
        ops::{
            EnsurePaths,
            release::{
                prepare_review,
                reservation::{ReleaseObservationJournal, tests::seed},
            },
        },
        policy::release::tests::fixture,
    },
    test_support::start_pocket_ic,
};
use canic_core::ids::{AppId, CanonicalNetworkId, FleetBinding, FleetId, FleetKey};
use ic_agent::identity::BasicIdentity;
use ic_testkit::pocket_ic::{CanisterSettings, PocketIcBuilder};
use std::{path::PathBuf, time::SystemTime};

fn reservation_journal(
    authority: &FleetReleaseAuthority,
    binding: &CapacityImportSourceBinding,
) -> (PathBuf, ReleaseObservationJournal) {
    let (mut review, mut observed) = fixture();
    let old_operator = review.authority.operator;
    let old_coordinator = review.authority.coordinator;
    review.authority = authority.clone();
    observed.authority = authority.clone();
    for source in &mut review.sources {
        if source.binding.canister_id == old_coordinator {
            source.binding = binding.clone();
        } else if source.binding.controllers == [old_operator] {
            source.binding.controllers = vec![authority.operator];
        }
    }
    for (sample, source) in observed.sources.iter_mut().zip(&review.sources) {
        sample.binding = source.binding.clone();
    }
    observed.owners[0].owner = authority.coordinator;
    for account in &mut review.accounts {
        account.ledger = authority.cycles_ledger;
        if account.owner == old_coordinator {
            account.owner = authority.coordinator;
        }
    }
    observed.accounts = review.accounts.clone();
    let review = prepare_review(review, &observed).unwrap();
    let directory = crate::test_support::temp_dir("release-observation-reserved");
    let paths = EnsurePaths::under(&directory, "local", "release");
    seed(&paths, &review);
    let journal = ReleaseObservationJournal::attach(&paths, &review, &observed).unwrap();
    (directory, journal)
}

fn authority(agent: &Agent) -> FleetReleaseAuthority {
    FleetReleaseAuthority {
        fleet: FleetBinding {
            app: AppId::from("release-observation"),
            fleet: FleetKey {
                canonical_network_id: CanonicalNetworkId::from_der_root_trust_anchor(
                    &agent.read_root_key(),
                )
                .unwrap(),
                fleet_id: FleetId::from_generated_bytes([1; 32]),
            },
        },
        network_root_key_sha256: Sha256::digest(agent.read_root_key()).into(),
        release_build_sha256: [2; 32],
        operation_id: [3; 32],
        operator: agent.get_principal().unwrap(),
        coordinator: Principal::from_slice(&[1]),
        cycles_ledger: Principal::from_slice(&[2]),
    }
}

fn source(
    canister_id: Principal,
    subnet: SubnetId,
    operator: Principal,
) -> CapacityImportSourceBinding {
    CapacityImportSourceBinding {
        canister_id,
        subnet,
        controllers: vec![operator],
        module_sha256: None,
        canister_version: 0,
        stopped: true,
        snapshots_size_bytes: 0,
    }
}

#[test]
fn physical_samples_reject_changed_versions_custody_and_snapshot_inventory() {
    let operator = Principal::from_slice(&[1]);
    let subnet = SubnetId::from_principal(Principal::from_slice(&[2]));
    let original = CapacityImportSampleRecord {
        binding: source(Principal::from_slice(&[3]), subnet, operator),
        cycles: 1000,
        reserved_cycles: 50,
    };
    let snapshots = vec![vec![1], vec![2]];
    let mut debited = original.clone();
    debited.cycles -= 5;
    assert_eq!(
        validate_physical_sample(
            operator, subnet, &original, &debited, &snapshots, &snapshots
        ),
        Ok(())
    );
    for mutate in [
        |sample: &mut CapacityImportSampleRecord| sample.binding.canister_version += 1,
        |sample: &mut CapacityImportSampleRecord| {
            sample.binding.controllers.push(Principal::from_slice(&[4]));
        },
        |sample: &mut CapacityImportSampleRecord| sample.binding.stopped = false,
        |sample: &mut CapacityImportSampleRecord| sample.binding.module_sha256 = Some([8; 32]),
    ] {
        let mut changed = original.clone();
        mutate(&mut changed);
        assert!(matches!(
            validate_physical_sample(
                operator, subnet, &original, &changed, &snapshots, &snapshots
            ),
            Err(FleetReleaseError::Custody { .. })
        ));
    }
    for changed in [vec![vec![1]], vec![vec![1], vec![1]], vec![vec![], vec![2]]] {
        assert!(matches!(
            validate_physical_sample(operator, subnet, &original, &original, &snapshots, &changed),
            Err(FleetReleaseError::Custody { .. })
        ));
    }
    for malformed in [vec![vec![1], vec![1]], vec![vec![], vec![2]]] {
        assert!(matches!(
            validate_physical_sample(
                operator, subnet, &original, &original, &malformed, &malformed
            ),
            Err(FleetReleaseError::Custody { .. })
        ));
    }
}

#[test]
#[ignore = "the workspace runner supplies one shared PocketIC server and serial execution"]
#[expect(
    clippy::too_many_lines,
    reason = "one real management journey proves authority binding and preparation-to-observation drift refusal"
)]
fn governed_pocketic_release_physical_observation_binds_custody() {
    let mut pic = start_pocket_ic(
        PocketIcBuilder::new()
            .with_nns_subnet()
            .with_application_subnet(),
    );
    pic.set_time(SystemTime::now().into());
    let identity = BasicIdentity::from_raw_key(&[17; 32]);
    let url = pic.make_live(None);
    let agent = Agent::builder()
        .with_url(url)
        .with_identity(identity)
        .with_max_response_body_size(SNAPSHOT_RESPONSE_BYTES)
        .build()
        .unwrap();
    agent.set_root_key(pic.root_key().unwrap());
    let mut authority = authority(&agent);
    let operator = authority.operator;
    let canister = pic.create_canister_with_settings(Some(operator), None);
    authority.coordinator = canister;
    pic.install_canister(
        canister,
        b"\0asm\x01\0\0\0".to_vec(),
        vec![],
        Some(operator),
    );
    pic.stop_canister(canister, Some(operator)).unwrap();
    let snapshot = pic
        .take_canister_snapshot(canister, Some(operator), None)
        .unwrap();
    let subnet = SubnetId::from_principal(pic.get_subnet(canister).unwrap());
    let binding = source(canister, subnet, operator);
    let (directory, mut journal) = reservation_journal(&authority, &binding);
    let runtime = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .unwrap();
    let mut observe = || {
        runtime.block_on(async {
            prepare_with_agent(agent.clone(), &authority, &binding)
                .await
                .unwrap()
                .observe(journal.reserve(canister).unwrap())
                .await
        })
    };
    let live = observe().unwrap();
    assert_eq!(live.snapshots, [snapshot.id]);
    assert!(live.sample.binding.stopped);
    assert_eq!(live.sample.binding.controllers, [operator]);
    assert!(live.sample.binding.module_sha256.is_some());
    assert!(live.sample.binding.snapshots_size_bytes > 0);
    assert!(live.sample.cycles > 0);
    let management = pic.canister_status(canister, Some(operator)).unwrap();
    assert_eq!(
        live.sample.reserved_cycles,
        u128::try_from(management.reserved_cycles.0).unwrap()
    );
    for change in [true, false] {
        let mut wrong = authority.clone();
        if change {
            wrong.operator = Principal::from_slice(&[9]);
        } else {
            wrong.network_root_key_sha256 = [9; 32];
        }
        assert!(matches!(
            runtime.block_on(prepare_with_agent(agent.clone(), &wrong, &binding)),
            Err(ReleaseObservationError::Authority)
        ));
    }
    let mut wrong_subnet = binding.clone();
    let mut wrong_network = authority.clone();
    wrong_network.fleet.fleet.canonical_network_id = CanonicalNetworkId::ic_mainnet();
    assert!(matches!(
        runtime.block_on(prepare_with_agent(agent.clone(), &wrong_network, &binding)),
        Err(ReleaseObservationError::Authority)
    ));
    wrong_subnet.subnet = SubnetId::from_principal(Principal::from_slice(&[9]));
    assert!(matches!(
        runtime.block_on(prepare_with_agent(agent.clone(), &authority, &wrong_subnet)),
        Err(ReleaseObservationError::Evidence(
            FleetReleaseError::Custody { .. }
        ))
    ));
    pic.start_canister(canister, Some(operator)).unwrap();
    assert!(matches!(
        observe(),
        Err(ReleaseObservationError::Evidence(
            FleetReleaseError::Custody { .. }
        ))
    ));
    pic.stop_canister(canister, Some(operator)).unwrap();
    let prepared = runtime
        .block_on(prepare_with_agent(agent.clone(), &authority, &binding))
        .unwrap();
    pic.update_canister_settings(
        canister,
        Some(operator),
        CanisterSettings {
            controllers: Some(vec![operator, Principal::from_slice(&[9])]),
            ..CanisterSettings::default()
        },
    )
    .unwrap();
    assert!(matches!(
        runtime.block_on(prepared.observe(journal.reserve(canister).unwrap())),
        Err(ReleaseObservationError::Management { .. })
    ));
    assert!(matches!(
        runtime.block_on(prepare_with_agent(agent.clone(), &authority, &binding)),
        Err(ReleaseObservationError::Evidence(
            FleetReleaseError::Custody { .. }
        ))
    ));
    drop(journal);
    std::fs::remove_dir_all(directory).unwrap();
}