canic-host 0.110.53

Host-side App build and desired-state Fleet reconciliation library for Canic workspaces
Documentation
use super::*;
use crate::fleet_ensure::{
    generate::{
        FreshEstateSeedRequest, initialize_fresh_estate_seed, tests::multi_component_source_toml,
    },
    workflow::readiness::{
        FleetReadinessError, FleetReadinessRequest, ReadinessGenerationInputs, inspect,
    },
};
use std::fs;

#[test]
fn completed_fresh_seed_rejects_before_build_or_network_and_explicit_inventory_passes() {
    let root = crate::test_support::temp_dir("generation-preflight");
    fs::create_dir_all(&root).unwrap();
    let source = root.join("policy.toml");
    let seed = root.join("seed.toml");
    let operator = Principal::from_slice(&[20]);
    let subnet = Principal::from_slice(&[21]).to_text();
    let ledger = Principal::from_text(super::super::mainnet_cycles_ledger()).unwrap();
    fs::write(
        &source,
        multi_component_source_toml(&operator.to_text(), &subnet, &subnet),
    )
    .unwrap();
    initialize_fresh_estate_seed(&FreshEstateSeedRequest {
        cycles_ledger: &ledger.to_text(),
        management_creation_fee_cycles: 500_000_000_000,
        source: &source,
        seed: &seed,
    })
    .unwrap();
    let request = FleetGenerationInputsRequest {
        root: &root,
        environment: "local",
        fleet: "fleet",
        source: &source,
        seed: &seed,
    };
    validate_generation_inputs(&request, operator, ledger).unwrap();
    assert!(!load(&request, None).unwrap().clean_reinstall);
    let paths = EnsurePaths::under(&root, "local", "fleet");
    fs::create_dir_all(paths.plan.parent().unwrap()).unwrap();
    for name in [".DS_Store", "notes.txt", "plan.json.bak", ".plan.json.tmp"] {
        fs::write(paths.plan.with_file_name(name), b"unrelated local metadata").unwrap();
    }
    validate_generation_inputs(&request, operator, ledger).unwrap();
    assert!(!load(&request, None).unwrap().clean_reinstall);
    write_completion(&paths);
    let original_plan = fs::read(&paths.plan).unwrap();
    let original_journal = fs::read(&paths.journal).unwrap();
    let original_seed = fs::read(&seed).unwrap();
    assert!(matches!(
        validate_generation_inputs(&request, operator, ledger),
        Err(FleetGenerateError::ResetRequiresExplicitInventory)
    ));
    let readiness = FleetReadinessRequest {
        workspace: &root,
        environment: "local",
        fleet: "fleet",
        icp_executable: "must-not-run",
        signing_identity: None,
        operator,
        cycles_ledger: ledger,
        estimated_required_cycles: None,
        desired: None,
        conversion: None,
        generation_inputs: Some(ReadinessGenerationInputs {
            source: &source,
            seed: &seed,
        }),
    };
    assert!(
        matches!(inspect(&readiness), Err(FleetReadinessError::GenerationInputs(error))
        if matches!(*error, FleetGenerateError::ResetRequiresExplicitInventory))
    );
    assert_eq!(fs::read(&seed).unwrap(), original_seed);
    assert_eq!(fs::read(&paths.plan).unwrap(), original_plan);
    assert_eq!(fs::read(&paths.journal).unwrap(), original_journal);
    assert!(!paths.lock.exists());

    let mut inventory: EstateSeed = load_toml(&seed, "seed").unwrap();
    inventory.fresh_estate = false;
    inventory.coordinator = Principal::from_slice(&[30]).to_text();
    inventory.roots[0].root = Principal::from_slice(&[31]).to_text();
    inventory.roots[0].store = Principal::from_slice(&[32]).to_text();
    inventory.roots[0].pool_imports = vec![Principal::from_slice(&[33]).to_text()];
    fs::write(&seed, toml::to_string(&inventory).unwrap()).unwrap();
    validate_generation_inputs(&request, operator, ledger).unwrap();
    let current = load(&request, None).unwrap();
    assert_reset_funding_projection(&request, &inventory, operator, ledger);
    assert!(current.clean_reinstall);
    assert_eq!(
        current.initialization,
        Some(BootstrapCoordinatorSelection::Initialize)
    );
    assert!(matches!(
        validate_generation_inputs(&request, Principal::from_slice(&[40]), ledger),
        Err(FleetGenerateError::Authority(_))
    ));
    assert!(matches!(
        validate_generation_inputs(&request, operator, Principal::from_slice(&[40])),
        Err(FleetGenerateError::Authority(_))
    ));

    inventory.roots[0].pool_imports = vec![inventory.coordinator.clone()];
    fs::write(&seed, toml::to_string(&inventory).unwrap()).unwrap();
    assert!(matches!(
        validate_generation_inputs(&request, operator, ledger),
        Err(FleetGenerateError::SeedTopology(_))
    ));
    assert_eq!(fs::read(&paths.plan).unwrap(), original_plan);
    assert_eq!(fs::read(&paths.journal).unwrap(), original_journal);

    assert_unreadable_predecessor(&request, &paths, &mut inventory, &readiness);
    fs::remove_dir_all(root).unwrap();
}

fn assert_unreadable_predecessor(
    request: &FleetGenerationInputsRequest<'_>,
    paths: &EnsurePaths,
    inventory: &mut EstateSeed,
    readiness: &FleetReadinessRequest<'_>,
) {
    let operator = readiness.operator;
    let ledger = readiness.cycles_ledger;
    inventory.roots[0].pool_imports = vec![Principal::from_slice(&[33]).to_text()];
    fs::write(request.seed, toml::to_string(&inventory).unwrap()).unwrap();
    fs::write(
        &paths.plan,
        b"unfinished predecessor plan, not current JSON",
    )
    .unwrap();
    fs::write(&paths.state, b"obsolete application schema").unwrap();
    fs::write(&paths.journal, b"malformed predecessor journal").unwrap();
    fs::write(
        paths.plan.with_file_name("capacity-import.json"),
        b"retained import",
    )
    .unwrap();
    validate_generation_inputs(request, operator, ledger).unwrap();
    assert_reset_funding_projection(request, inventory, operator, ledger);
    assert!(load(request, None).unwrap().clean_reinstall);
    assert!(
        crate::fleet_ensure::workflow::clean_reinstall::selected(
            request.root,
            "local",
            "fleet",
            true,
            false
        )
        .unwrap()
    );
    assert_eq!(
        fs::read(&paths.plan).unwrap(),
        b"unfinished predecessor plan, not current JSON"
    );
    // Readiness proceeds to current network selection, not predecessor decoding.
    assert!(matches!(
        inspect(readiness),
        Err(FleetReadinessError::Network(_))
    ));
}

fn write_completion(paths: &EnsurePaths) {
    fs::create_dir_all(paths.plan.parent().unwrap()).unwrap();
    let operation = "ab".repeat(32);
    let digest = "cd".repeat(32);
    fs::write(
        &paths.plan,
        serde_json::to_vec(&serde_json::json!({
            "environment": "local", "fleet": "fleet", "operation_id": operation,
            "plan_sha256": digest, "scope": "full",
        }))
        .unwrap(),
    )
    .unwrap();
    fs::write(
        &paths.journal,
        serde_json::to_vec(&serde_json::json!({
            "fleet": "fleet", "operation_id": operation, "plan_sha256": digest,
            "completion": "converged", "effects": [{"state": "applied"}],
        }))
        .unwrap(),
    )
    .unwrap();
}

fn assert_reset_funding_projection(
    request: &FleetGenerationInputsRequest<'_>,
    inventory: &EstateSeed,
    operator: Principal,
    ledger: Principal,
) {
    let targets = bootstrap_funding_targets(request, operator, ledger).unwrap();
    assert!(matches!(
        bootstrap_funding_targets(request, Principal::anonymous(), ledger),
        Err(FleetGenerateError::Authority(_))
    ));
    assert!(matches!(
        bootstrap_funding_targets(request, operator, Principal::anonymous()),
        Err(FleetGenerateError::Authority(_))
    ));
    assert_eq!(
        targets
            .iter()
            .map(|target| target.principal.as_str())
            .collect::<Vec<_>>(),
        [
            &inventory.coordinator,
            &inventory.roots[0].root,
            &inventory.roots[0].store
        ]
    );
    assert!(targets.iter().all(|target| target.observation_burn_cycles
        == super::super::GENERATED_RETAINED_MAXIMUM_OBSERVATION_BURN_CYCLES));
    assert_eq!(targets[2].minimum_cycles, 0);
}