canic-host 0.110.45

Host-side App build and desired-state Fleet reconciliation library for Canic workspaces
Documentation
//! Completed receipt audit checks current-schema receipts without external mutation.

use super::*;
use crate::fleet_ensure::ops::retained_contract::check;
use std::{fs, path::PathBuf};

#[test]
#[ignore = "requires explicitly supplied read-only completed source records"]
fn inspect_supplied_completed_receipts_without_external_mutation() {
    let root = std::env::var_os("CANIC_COMPLETED_SOURCE_WORKSPACE").unwrap();
    let environment = std::env::var("CANIC_COMPLETED_SOURCE_ENVIRONMENT").unwrap();
    let fleet = std::env::var("CANIC_COMPLETED_SOURCE_FLEET").unwrap();
    let paths = EnsurePaths::under(std::path::Path::new(&root), &environment, &fleet);
    let audit = inspect(&paths, &environment, &fleet).unwrap();
    assert!(audit.effect_count > 0);
    assert!(audit.phase_count > 0);
}

fn completed_fixture() -> (PathBuf, EnsurePaths) {
    let (fixture, paths, _) = crate::fleet_ensure::tests::terminal_retirement_fixture();
    let mut plan = read_json(&paths.plan);
    plan["recovery_review"] = Value::Null;
    plan["reviewed_desired"]["desired"]["bootstrap"] = serde_json::json!({
        "admission_identity_origin": null,
        "admission": {"schema_version": 1, "fleet_principals": [], "rules": [], "template_digest": vec![0; 32]},
        "app": "receipt_test", "canonical_network_id": "11".repeat(32),
        "component_deployment_configuration": {
            "component_topology": {"component_specs": [], "provisioning_grants": []},
            "component_group_topology": {"component_groups": []},
            "deployment_topology": {"component_group_deployments": []},
            "fleet_service_topology": {"targets": []},
        },
        "coordinator": fixture.desired.canisters[0].principal,
        "coordinator_subnet": fixture.desired.canisters[0].subnet,
        "fleet_id": "22".repeat(32), "fresh_estate": false,
        "release_build_id": "33".repeat(32), "root_funding": null, "roots": [],
        "recovery_controllers": [],
    });
    let mut plan: FleetEnsurePlan = decode(plan).unwrap();
    plan.plan_sha256 = expected_plan_sha256(&plan);
    let mut journal = read_json(&paths.journal);
    journal["plan_sha256"] = plan.plan_sha256.clone().into();
    let mut actions = initial_actions(&plan.canisters).unwrap();
    for reference in journal["successor_phases"].as_array_mut().unwrap() {
        let old = reference["plan_sha256"].as_str().unwrap();
        let mut phase = read_json(
            &paths
                .plan
                .with_file_name("phases")
                .join(format!("{old}.json")),
        );
        phase["reviewed_desired"] = json::to_value(&plan.reviewed_desired).unwrap();
        let mut phase: FleetEnsurePlan = decode(phase).unwrap();
        phase.plan_sha256 = expected_plan_sha256(&phase);
        reference["plan_sha256"] = phase.plan_sha256.clone().into();
        write_json(
            &paths
                .plan
                .with_file_name("phases")
                .join(format!("{}.json", phase.plan_sha256)),
            &json::to_value(&phase).unwrap(),
        );
        actions.extend(phase.protocol_actions);
    }
    for (effect, action) in journal["effects"]
        .as_array_mut()
        .unwrap()
        .iter_mut()
        .zip(&actions)
    {
        effect["action_sha256"] = sha256_hex(&json::to_vec(action).unwrap()).into();
    }
    write_json(&paths.plan, &json::to_value(&plan).unwrap());
    write_json(&paths.journal, &journal);
    (fixture.root, paths)
}

fn read_json(path: &std::path::Path) -> Value {
    serde_json::from_slice(&fs::read(path).unwrap()).unwrap()
}
fn write_json(path: &std::path::Path, value: &Value) {
    fs::write(path, serde_json::to_vec_pretty(value).unwrap()).unwrap();
}

#[test]
fn completed_receipt_audit_does_not_supply_missing_estate_authority() {
    let (root, paths) = completed_fixture();
    let before: Vec<_> = [&paths.plan, &paths.journal, &paths.state]
        .into_iter()
        .map(|path| fs::read(path).unwrap())
        .collect();
    let audit = inspect(&paths, "local", "test-fleet").unwrap();
    assert_eq!(audit.recorded_funding_cycles, 110);
    assert_eq!(audit.recorded_operator_debit_cycles, 120);
    assert_eq!(audit.original_maximum_execution_burn_cycles, 200);
    // This receipt-only fixture has no complete Root inventory. A valid receipt
    // audit cannot manufacture that separate authority or pass the full preflight.
    check(&root, "local", "test-fleet").unwrap();
    assert!(
        crate::fleet_ensure::ops::retained_contract::completed_source_available(
            &root,
            "local",
            "test-fleet"
        )
        .unwrap()
    );
    assert!(matches!(
        super::super::inventory::inspect(&paths, "local", "test-fleet"),
        Err(EnsureStateError::InvalidTerminalSource)
    ));
    assert_eq!(fs::read(&paths.plan).unwrap(), before[0]);
    assert_eq!(fs::read(&paths.journal).unwrap(), before[1]);
    assert_eq!(fs::read(&paths.state).unwrap(), before[2]);
    fs::remove_dir_all(root).unwrap();
}

#[test]
fn original_root_balances_bind_the_declared_ledger_and_exact_owner() {
    use crate::fleet_ensure::model::EstateFundingDomainPlan;

    let (root, paths) = completed_fixture();
    let plan: FleetEnsurePlan = decode(read_json(&paths.plan)).unwrap();
    let mut desired = plan.reviewed_desired.unwrap().into_desired();
    desired.canisters[0].kind = DesiredCanisterKind::Root;
    let declaration = &desired.canisters[0];
    let domain = EstateFundingDomainPlan {
        allocated_workloads: 0,
        available_cycles: Some(30),
        available_pool_slots: 0,
        creation_amount_cycles: 50,
        cycles_ledger: desired.cycles_ledger.clone(),
        creation_execution_margin_cycles: 0,
        readiness_floor_cycles: 20,
        eligible_ready_pool_assets: 0,
        initial_pool_assets: Vec::new(),
        ledger_fee_cycles: 10,
        management_creation_fee_cycles: 50,
        maximum_creation_debit_cycles: 0,
        maximum_creation_fee_cycles: 0,
        maximum_funding_cycles: 0,
        occupied_pool_assets: 0,
        pending_creation_count: 0,
        pending_creation: None,
        pool_maximum_size: 1,
        planned_initial_workloads: 0,
        required_creation_count: 0,
        root: declaration.name.clone(),
        root_principal: declaration.principal.clone(),
        shortfall_cycles: 0,
    };
    let journal = serde_json::json!({
        "initial_estate_funding_cycles_by_root": { &domain.root: "30" },
    });
    let mut bounds = plan.conservation;
    bounds.estate_funding_domains = vec![domain.clone()];
    assert_eq!(
        initial_root_accounts(&journal, &bounds, &desired).unwrap(),
        BTreeMap::from([(domain.root, 30)])
    );
    for mutation in ["ledger", "owner", "absent-owner", "kind", "duplicate"] {
        let mut changed_bounds = bounds.clone();
        let mut changed_desired = desired.clone();
        let domain = &mut changed_bounds.estate_funding_domains[0];
        match mutation {
            "ledger" => domain.cycles_ledger = changed_desired.operator.clone(),
            "owner" => domain.root_principal = Some(changed_desired.operator.clone()),
            "absent-owner" => domain.root_principal = None,
            "kind" => changed_desired.canisters[0].kind = DesiredCanisterKind::Coordinator,
            "duplicate" => changed_desired
                .canisters
                .push(changed_desired.canisters[0].clone()),
            _ => unreachable!(),
        }
        assert!(
            matches!(
                initial_root_accounts(&journal, &changed_bounds, &changed_desired),
                Err(EnsureStateError::InvalidTerminalSource)
            ),
            "{mutation}"
        );
    }
    fs::remove_dir_all(root).unwrap();
}

#[test]
fn incomplete_changed_and_overbudget_receipts_reject() {
    for (pointer, value) in [
        ("/completion", Value::from("in_progress")),
        ("/effects/0/state", Value::from("issued")),
        ("/effects/0/action_sha256", Value::from("00".repeat(32))),
        ("/effects/0/maintenance_attempts", Value::from(1)),
        ("/effects/0/publication_attempts", Value::from(1)),
        ("/effects/0/receipt", Value::Null),
        ("/initial_controlled_cycles", Value::from("invalid")),
        (
            "/initial_controlled_cycles",
            Value::from(u128::MAX.to_string()),
        ),
        ("/initial_operator_cycles", Value::from("119")),
        ("/effects/0/post_cycles", Value::from("499")),
        (
            "/successor_phases/0/execution_burn_before_phase",
            Value::from("201"),
        ),
    ] {
        let (root, paths) = completed_fixture();
        let mut journal = read_json(&paths.journal);
        *journal.pointer_mut(pointer).unwrap() = value;
        write_json(&paths.journal, &journal);
        assert!(matches!(
            inspect(&paths, "local", "test-fleet"),
            Err(EnsureStateError::InvalidTerminalSource)
        ));
        fs::remove_dir_all(root).unwrap();
    }
}

#[test]
fn extra_receipts_unknown_paid_fields_and_invented_controllers_reject() {
    for mutation in [
        "extra-effect",
        "unknown-paid-field",
        "controller-declaration",
        "budget",
    ] {
        let (root, paths) = completed_fixture();
        let mut journal = read_json(&paths.journal);
        let mut plan = read_json(&paths.plan);
        match mutation {
            "extra-effect" => {
                let duplicate = journal["effects"][0].clone();
                journal["effects"].as_array_mut().unwrap().push(duplicate);
            }
            "unknown-paid-field" => {
                journal["unaccounted_payment"] = serde_json::json!({"amount":"1"});
            }
            "controller-declaration" => {
                plan["reviewed_desired"]["desired"]["bootstrap"]["recovery_controllers"] =
                    serde_json::json!(["aaaaa-aa"]);
            }
            "budget" => {
                plan["conservation"]["maximum_execution_burn_cycles"] = "999".into();
            }
            _ => unreachable!(),
        }
        write_json(&paths.plan, &plan);
        write_json(&paths.journal, &journal);
        assert!(matches!(
            inspect(&paths, "local", "test-fleet"),
            Err(EnsureStateError::InvalidTerminalSource)
        ));
        fs::remove_dir_all(root).unwrap();
    }
}

#[test]
fn changed_successor_payload_cannot_reuse_its_completed_phase_digest() {
    let (root, paths) = completed_fixture();
    let journal = read_json(&paths.journal);
    let label = journal["successor_phases"][0]["plan_sha256"]
        .as_str()
        .unwrap();
    let path = paths
        .plan
        .with_file_name("phases")
        .join(format!("{label}.json"));
    let mut phase = read_json(&path);
    phase["protocol_actions"][0]["name"] = "substituted-owner".into();
    write_json(&path, &phase);
    assert!(matches!(
        inspect(&paths, "local", "test-fleet"),
        Err(EnsureStateError::InvalidTerminalSource)
    ));
    fs::remove_dir_all(root).unwrap();
}